From b2c458cfcbfc5781f38203dd27f9fbe90577ebea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:08:25 +0900 Subject: [PATCH 001/118] test(core): require bounded BiDi response envelope parsing --- ...webdriver_bidi_response_envelope_parser.rs | 262 ++++++++++++++++++ 1 file changed, 262 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_response_envelope_parser.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_response_envelope_parser.rs b/crates/originweave-core/tests/webdriver_bidi_response_envelope_parser.rs new file mode 100644 index 000000000..b09d2461b --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_response_envelope_parser.rs @@ -0,0 +1,262 @@ +use std::error::Error; + +use originweave_core::{ + BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_COMMAND_ID, + MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH, MAX_WEBDRIVER_BIDI_RESPONSE_TOP_LEVEL_FIELDS, + ParsedWebDriverBiDiCommandResponseEnvelope, WebDriverBiDiCommandResponseKind, + WebDriverBiDiResponseEnvelopeParseError, +}; + +#[test] +fn parser_classifies_exact_success_and_nullable_error_envelopes() -> Result<(), Box> { + let success_raw = + " {\"type\":\"success\",\"id\":42,\"result\":{\"nodes\":[]}}\r\n"; + let success = BoundedWebDriverBiDiResponseDocument::new(success_raw)? + .parse_command_response()?; + assert_eq!(success.kind(), WebDriverBiDiCommandResponseKind::Success); + assert_eq!(success.response_id(), Some(42)); + assert_eq!(success.as_str(), success_raw); + + let error_raw = + "{\"type\":\"error\",\"id\":null,\"error\":\"invalid argument\",\"message\":\"bad request\"}"; + let error = BoundedWebDriverBiDiResponseDocument::new(error_raw)? + .parse_command_response()?; + assert_eq!(error.kind(), WebDriverBiDiCommandResponseKind::Error); + assert_eq!(error.response_id(), None); + assert_eq!(error.as_str(), error_raw); + Ok(()) +} + +#[test] +fn parser_accepts_extensible_fields_only_when_the_complete_json_is_valid() +-> Result<(), Box> { + let raw = concat!( + "{\"vendor\":{\"nested\":[true,false,null,{\"text\":\"a\\\\b\\\"c\\u263a\"}]},", + "\"id\":7,\"result\":{},\"type\":\"success\"}" + ); + let parsed = BoundedWebDriverBiDiResponseDocument::new(raw)? + .parse_command_response()?; + assert_eq!(parsed.response_id(), Some(7)); + + for malformed in [ + "{\"type\":\"success\",\"id\":7,\"result\":{},}", + "{\"type\":\"success\",\"id\":7,\"result\":{},\"x\":01}", + "{\"type\":\"success\",\"id\":7,\"result\":{},\"x\":\"\\q\"}", + "{\"type\":\"success\",\"id\":7,\"result\":{},\"x\":[1,]}", + ] { + let document = BoundedWebDriverBiDiResponseDocument::new(malformed)?; + assert_eq!( + document.parse_command_response(), + Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson) + ); + } + Ok(()) +} + +#[test] +fn parser_rejects_missing_duplicate_or_unexpected_response_discriminators() +-> Result<(), Box> { + for (raw, expected) in [ + ( + "{\"id\":1,\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::MissingResponseType, + ), + ( + "{\"type\":\"event\",\"id\":1,\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::UnexpectedResponseType, + ), + ( + "{\"type\":\"success\",\"type\":\"error\",\"id\":1,\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::DuplicateTopLevelField, + ), + ( + "{\"type\":\"success\",\"id\":1,\"id\":1,\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::DuplicateTopLevelField, + ), + ] { + let document = BoundedWebDriverBiDiResponseDocument::new(raw)?; + assert_eq!(document.parse_command_response(), Err(expected)); + } + Ok(()) +} + +#[test] +fn parser_requires_a_present_protocol_range_id_and_success_result() +-> Result<(), Box> { + for (raw, expected) in [ + ( + "{\"type\":\"success\",\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::MissingResponseId, + ), + ( + "{\"type\":\"error\",\"error\":\"invalid argument\",\"message\":\"bad\"}", + WebDriverBiDiResponseEnvelopeParseError::MissingResponseId, + ), + ( + "{\"type\":\"success\",\"id\":null,\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId, + ), + ( + "{\"type\":\"success\",\"id\":-1,\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId, + ), + ( + "{\"type\":\"success\",\"id\":1.0,\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId, + ), + ( + "{\"type\":\"success\",\"id\":1e0,\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId, + ), + ( + "{\"type\":\"success\",\"id\":9007199254740992,\"result\":{}}", + WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId, + ), + ( + "{\"type\":\"success\",\"id\":1}", + WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload, + ), + ( + "{\"type\":\"success\",\"id\":1,\"result\":[]}", + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ), + ] { + let document = BoundedWebDriverBiDiResponseDocument::new(raw)?; + assert_eq!(document.parse_command_response(), Err(expected)); + } + + let maximum = format!( + "{{\"type\":\"success\",\"id\":{MAX_WEBDRIVER_BIDI_COMMAND_ID},\"result\":{{}}}}" + ); + assert_eq!( + BoundedWebDriverBiDiResponseDocument::new(&maximum)? + .parse_command_response()? + .response_id(), + Some(MAX_WEBDRIVER_BIDI_COMMAND_ID) + ); + Ok(()) +} + +#[test] +fn parser_requires_error_code_message_and_string_stacktrace() -> Result<(), Box> { + for (raw, expected) in [ + ( + "{\"type\":\"error\",\"id\":1,\"message\":\"bad\"}", + WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload, + ), + ( + "{\"type\":\"error\",\"id\":1,\"error\":\"invalid argument\"}", + WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload, + ), + ( + "{\"type\":\"error\",\"id\":1,\"error\":1,\"message\":\"bad\"}", + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ), + ( + "{\"type\":\"error\",\"id\":1,\"error\":\"invalid argument\",\"message\":false}", + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ), + ( + "{\"type\":\"error\",\"id\":1,\"error\":\"invalid argument\",\"message\":\"bad\",\"stacktrace\":[]}", + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ), + ] { + let document = BoundedWebDriverBiDiResponseDocument::new(raw)?; + assert_eq!(document.parse_command_response(), Err(expected)); + } + + let valid = BoundedWebDriverBiDiResponseDocument::new( + "{\"type\":\"error\",\"id\":7,\"error\":\"invalid argument\",\"message\":\"bad\",\"stacktrace\":\"frame\"}", + )? + .parse_command_response()?; + assert_eq!(valid.response_id(), Some(7)); + Ok(()) +} + +#[test] +fn parser_enforces_top_level_field_and_json_depth_budgets() -> Result<(), Box> { + let mut fields = vec![ + "\"type\":\"success\"".to_owned(), + "\"id\":1".to_owned(), + "\"result\":{}".to_owned(), + ]; + while fields.len() < MAX_WEBDRIVER_BIDI_RESPONSE_TOP_LEVEL_FIELDS { + fields.push(format!("\"x{}\":null", fields.len())); + } + let exact_fields = format!("{{{}}}", fields.join(",")); + assert!( + BoundedWebDriverBiDiResponseDocument::new(&exact_fields)? + .parse_command_response() + .is_ok() + ); + fields.push("\"overflow\":null".to_owned()); + let over_fields = format!("{{{}}}", fields.join(",")); + assert_eq!( + BoundedWebDriverBiDiResponseDocument::new(&over_fields)? + .parse_command_response(), + Err(WebDriverBiDiResponseEnvelopeParseError::TopLevelFieldCountExceeded) + ); + + let exact_nested = format!( + "{{\"type\":\"success\",\"id\":1,\"result\":{{\"x\":{}{}}}}}", + "[".repeat(MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH - 2), + "]".repeat(MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH - 2) + ); + assert!( + BoundedWebDriverBiDiResponseDocument::new(&exact_nested)? + .parse_command_response() + .is_ok() + ); + + let over_nested = format!( + "{{\"type\":\"success\",\"id\":1,\"result\":{{\"x\":{}{}}}}}", + "[".repeat(MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH - 1), + "]".repeat(MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH - 1) + ); + assert_eq!( + BoundedWebDriverBiDiResponseDocument::new(&over_nested)? + .parse_command_response(), + Err(WebDriverBiDiResponseEnvelopeParseError::JsonDepthExceeded) + ); + Ok(()) +} + +#[test] +fn parser_normalizes_escaped_top_level_names_before_duplicate_detection() +-> Result<(), Box> { + let duplicate = BoundedWebDriverBiDiResponseDocument::new( + "{\"type\":\"success\",\"\\u0069d\":1,\"id\":1,\"result\":{}}", + )?; + assert_eq!( + duplicate.parse_command_response(), + Err(WebDriverBiDiResponseEnvelopeParseError::DuplicateTopLevelField) + ); + + let unicode_extension = BoundedWebDriverBiDiResponseDocument::new( + "{\"type\":\"success\",\"id\":1,\"result\":{},\"메타\":\"값\"}", + )? + .parse_command_response()?; + assert_eq!(unicode_extension.response_id(), Some(1)); + Ok(()) +} + +#[test] +fn response_envelope_parse_errors_are_deterministic_and_source_free() { + for error in [ + WebDriverBiDiResponseEnvelopeParseError::InvalidJson, + WebDriverBiDiResponseEnvelopeParseError::JsonDepthExceeded, + WebDriverBiDiResponseEnvelopeParseError::TopLevelFieldCountExceeded, + WebDriverBiDiResponseEnvelopeParseError::DuplicateTopLevelField, + WebDriverBiDiResponseEnvelopeParseError::MissingResponseType, + WebDriverBiDiResponseEnvelopeParseError::UnexpectedResponseType, + WebDriverBiDiResponseEnvelopeParseError::MissingResponseId, + WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId, + WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload, + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ] { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_none()); + } +} + +fn _parsed_type_is_public(_parsed: ParsedWebDriverBiDiCommandResponseEnvelope) {} From 89bd3db5d5f2ee66c1e5599c3a47dcfac1d40ebd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:13:25 +0900 Subject: [PATCH 002/118] style(core): apply canonical BiDi parser test formatting --- ...webdriver_bidi_response_envelope_parser.rs | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_response_envelope_parser.rs b/crates/originweave-core/tests/webdriver_bidi_response_envelope_parser.rs index b09d2461b..d15a525b0 100644 --- a/crates/originweave-core/tests/webdriver_bidi_response_envelope_parser.rs +++ b/crates/originweave-core/tests/webdriver_bidi_response_envelope_parser.rs @@ -9,18 +9,15 @@ use originweave_core::{ #[test] fn parser_classifies_exact_success_and_nullable_error_envelopes() -> Result<(), Box> { - let success_raw = - " {\"type\":\"success\",\"id\":42,\"result\":{\"nodes\":[]}}\r\n"; - let success = BoundedWebDriverBiDiResponseDocument::new(success_raw)? - .parse_command_response()?; + let success_raw = " {\"type\":\"success\",\"id\":42,\"result\":{\"nodes\":[]}}\r\n"; + let success = + BoundedWebDriverBiDiResponseDocument::new(success_raw)?.parse_command_response()?; assert_eq!(success.kind(), WebDriverBiDiCommandResponseKind::Success); assert_eq!(success.response_id(), Some(42)); assert_eq!(success.as_str(), success_raw); - let error_raw = - "{\"type\":\"error\",\"id\":null,\"error\":\"invalid argument\",\"message\":\"bad request\"}"; - let error = BoundedWebDriverBiDiResponseDocument::new(error_raw)? - .parse_command_response()?; + let error_raw = "{\"type\":\"error\",\"id\":null,\"error\":\"invalid argument\",\"message\":\"bad request\"}"; + let error = BoundedWebDriverBiDiResponseDocument::new(error_raw)?.parse_command_response()?; assert_eq!(error.kind(), WebDriverBiDiCommandResponseKind::Error); assert_eq!(error.response_id(), None); assert_eq!(error.as_str(), error_raw); @@ -34,8 +31,7 @@ fn parser_accepts_extensible_fields_only_when_the_complete_json_is_valid() "{\"vendor\":{\"nested\":[true,false,null,{\"text\":\"a\\\\b\\\"c\\u263a\"}]},", "\"id\":7,\"result\":{},\"type\":\"success\"}" ); - let parsed = BoundedWebDriverBiDiResponseDocument::new(raw)? - .parse_command_response()?; + let parsed = BoundedWebDriverBiDiResponseDocument::new(raw)?.parse_command_response()?; assert_eq!(parsed.response_id(), Some(7)); for malformed in [ @@ -81,8 +77,7 @@ fn parser_rejects_missing_duplicate_or_unexpected_response_discriminators() } #[test] -fn parser_requires_a_present_protocol_range_id_and_success_result() --> Result<(), Box> { +fn parser_requires_a_present_protocol_range_id_and_success_result() -> Result<(), Box> { for (raw, expected) in [ ( "{\"type\":\"success\",\"result\":{}}", @@ -125,9 +120,8 @@ fn parser_requires_a_present_protocol_range_id_and_success_result() assert_eq!(document.parse_command_response(), Err(expected)); } - let maximum = format!( - "{{\"type\":\"success\",\"id\":{MAX_WEBDRIVER_BIDI_COMMAND_ID},\"result\":{{}}}}" - ); + let maximum = + format!("{{\"type\":\"success\",\"id\":{MAX_WEBDRIVER_BIDI_COMMAND_ID},\"result\":{{}}}}"); assert_eq!( BoundedWebDriverBiDiResponseDocument::new(&maximum)? .parse_command_response()? @@ -192,8 +186,7 @@ fn parser_enforces_top_level_field_and_json_depth_budgets() -> Result<(), Box Result<(), Box Date: Tue, 18 Aug 2026 10:18:32 +0900 Subject: [PATCH 003/118] feat(core): parse bounded BiDi response envelopes --- .../src/webdriver_bidi_response_envelope.rs | 595 ++++++++++++++++++ 1 file changed, 595 insertions(+) create mode 100644 crates/originweave-core/src/webdriver_bidi_response_envelope.rs diff --git a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs new file mode 100644 index 000000000..0dc2c5add --- /dev/null +++ b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs @@ -0,0 +1,595 @@ +use std::{error::Error, fmt}; + +use crate::{ + BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_COMMAND_ID, + WebDriverBiDiCommandResponseKind, +}; + +/// Maximum accepted JSON container nesting depth for one WebDriver BiDi response document. +/// +/// The top-level response object is depth 1. The limit is an OriginWeave resource-safety +/// budget, not a WebDriver BiDi protocol maximum. +pub const MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH: usize = 64; + +/// Maximum accepted number of fields in one top-level WebDriver BiDi response object. +/// +/// The limit is an OriginWeave resource-safety budget, not a WebDriver BiDi protocol maximum. +pub const MAX_WEBDRIVER_BIDI_RESPONSE_TOP_LEVEL_FIELDS: usize = 64; + +/// Fail-closed reasons a bounded WebDriver BiDi response document cannot become typed envelope +/// evidence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiResponseEnvelopeParseError { + /// The document is not syntactically valid JSON with one complete top-level object. + InvalidJson, + /// JSON object/array nesting exceeded the configured parser safety budget. + JsonDepthExceeded, + /// The top-level response object contains more fields than the configured safety budget. + TopLevelFieldCountExceeded, + /// The top-level response object repeats a field after JSON string escape decoding. + DuplicateTopLevelField, + /// The response object omits the required `type` discriminator. + MissingResponseType, + /// The response `type` is not exactly `success` or `error`. + UnexpectedResponseType, + /// The response object omits the required `id` field. + MissingResponseId, + /// The response `id` is not a protocol-range JSON integer, or is `null` where forbidden. + InvalidResponseId, + /// The selected response kind omits one of its required payload fields. + MissingRequiredPayload, + /// A required response payload field has the wrong JSON value type. + InvalidRequiredPayloadType, +} + +impl fmt::Display for WebDriverBiDiResponseEnvelopeParseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::InvalidJson => "WebDriver BiDi response document is not valid JSON", + Self::JsonDepthExceeded => "WebDriver BiDi response JSON depth exceeds the safety budget", + Self::TopLevelFieldCountExceeded => { + "WebDriver BiDi response top-level field count exceeds the safety budget" + } + Self::DuplicateTopLevelField => { + "WebDriver BiDi response contains a duplicate top-level field" + } + Self::MissingResponseType => "WebDriver BiDi response is missing its type field", + Self::UnexpectedResponseType => "WebDriver BiDi response type is not success or error", + Self::MissingResponseId => "WebDriver BiDi response is missing its id field", + Self::InvalidResponseId => "WebDriver BiDi response id is invalid", + Self::MissingRequiredPayload => { + "WebDriver BiDi response is missing a required payload field" + } + Self::InvalidRequiredPayloadType => { + "WebDriver BiDi response payload field has an invalid JSON type" + } + }) + } +} + +impl Error for WebDriverBiDiResponseEnvelopeParseError {} + +/// Typed evidence that one bounded raw document is a syntactically valid WebDriver BiDi command +/// response envelope. +/// +/// The value retains the exact admitted wire text and exposes only the command-response kind and +/// parsed response identifier needed by later correlation. Parsing does not authenticate a browser +/// or transport and does not grant browser, node, policy, or Agent authority. +#[derive(Debug, PartialEq, Eq)] +pub struct ParsedWebDriverBiDiCommandResponseEnvelope { + document: BoundedWebDriverBiDiResponseDocument, + kind: WebDriverBiDiCommandResponseKind, + response_id: Option, +} + +impl ParsedWebDriverBiDiCommandResponseEnvelope { + /// Returns whether the parsed command response is a success or error envelope. + #[must_use] + pub const fn kind(&self) -> WebDriverBiDiCommandResponseKind { + self.kind + } + + /// Returns the parsed command identifier, or `None` only for an error response whose required + /// `id` field was explicitly JSON `null`. + #[must_use] + pub const fn response_id(&self) -> Option { + self.response_id + } + + /// Returns the exact bounded wire text from which this envelope evidence was parsed. + #[must_use] + pub fn as_str(&self) -> &str { + self.document.as_str() + } +} + +impl BoundedWebDriverBiDiResponseDocument { + /// Parses this already-bounded raw document into typed command-response envelope evidence. + /// + /// Complete JSON syntax, decoded top-level field uniqueness, response-kind requirements, + /// protocol-range response identifiers, and explicit parser resource budgets are enforced + /// before the value can be used by a later correlation boundary. + pub fn parse_command_response( + self, + ) -> Result + { + let parsed = ResponseEnvelopeParser::new(self.as_str()).parse()?; + Ok(ParsedWebDriverBiDiCommandResponseEnvelope { + document: self, + kind: parsed.kind, + response_id: parsed.response_id, + }) + } +} + +#[derive(Debug, PartialEq, Eq)] +enum ParsedJsonValue { + Object, + Array, + String(Vec), + Number(String), + Boolean, + Null, +} + +struct ParsedEnvelopeFields { + kind: WebDriverBiDiCommandResponseKind, + response_id: Option, +} + +struct ResponseEnvelopeParser<'input> { + input: &'input str, + position: usize, +} + +impl<'input> ResponseEnvelopeParser<'input> { + const fn new(input: &'input str) -> Self { + Self { input, position: 0 } + } + + fn parse( + mut self, + ) -> Result { + self.skip_whitespace(); + self.expect_byte(b'{')?; + self.skip_whitespace(); + + let mut seen_fields: Vec> = Vec::new(); + let mut response_type = None; + let mut response_id = None; + let mut result = None; + let mut error_code = None; + let mut message = None; + let mut stacktrace = None; + + if self.peek_byte() != Some(b'}') { + loop { + if seen_fields.len() >= MAX_WEBDRIVER_BIDI_RESPONSE_TOP_LEVEL_FIELDS { + return Err( + WebDriverBiDiResponseEnvelopeParseError::TopLevelFieldCountExceeded, + ); + } + + let field_name = self.parse_string()?; + if seen_fields.contains(&field_name) { + return Err(WebDriverBiDiResponseEnvelopeParseError::DuplicateTopLevelField); + } + seen_fields.push(field_name.clone()); + self.skip_whitespace(); + self.expect_byte(b':')?; + self.skip_whitespace(); + let value = self.parse_value(2)?; + + match field_name.as_slice() { + b"type" => response_type = Some(value), + b"id" => response_id = Some(value), + b"result" => result = Some(value), + b"error" => error_code = Some(value), + b"message" => message = Some(value), + b"stacktrace" => stacktrace = Some(value), + _ => {} + } + + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + } + Some(b'}') => break, + _ => return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson), + } + } + } + + self.expect_byte(b'}')?; + self.skip_whitespace(); + if self.position != self.input.len() { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + + let kind = Self::parse_response_type(response_type)?; + let response_id = Self::parse_response_id(response_id, kind)?; + Self::validate_required_payload(kind, result, error_code, message, stacktrace)?; + + Ok(ParsedEnvelopeFields { kind, response_id }) + } + + fn parse_response_type( + value: Option, + ) -> Result { + let value = value.ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingResponseType)?; + match value { + ParsedJsonValue::String(value) if value == b"success" => { + Ok(WebDriverBiDiCommandResponseKind::Success) + } + ParsedJsonValue::String(value) if value == b"error" => { + Ok(WebDriverBiDiCommandResponseKind::Error) + } + _ => Err(WebDriverBiDiResponseEnvelopeParseError::UnexpectedResponseType), + } + } + + fn parse_response_id( + value: Option, + kind: WebDriverBiDiCommandResponseKind, + ) -> Result, WebDriverBiDiResponseEnvelopeParseError> { + let value = value.ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingResponseId)?; + let raw = match value { + ParsedJsonValue::Null if kind == WebDriverBiDiCommandResponseKind::Error => { + return Ok(None); + } + ParsedJsonValue::Number(raw) => raw, + _ => return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId), + }; + if !raw.bytes().all(|byte| byte.is_ascii_digit()) { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId); + } + let parsed = raw + .parse::() + .map_err(|_| WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId)?; + if parsed > MAX_WEBDRIVER_BIDI_COMMAND_ID { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId); + } + Ok(Some(parsed)) + } + + fn validate_required_payload( + kind: WebDriverBiDiCommandResponseKind, + result: Option, + error_code: Option, + message: Option, + stacktrace: Option, + ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { + match kind { + WebDriverBiDiCommandResponseKind::Success => { + let result = + result.ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; + if !matches!(result, ParsedJsonValue::Object) { + return Err( + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ); + } + } + WebDriverBiDiCommandResponseKind::Error => { + let error_code = error_code + .ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; + let message = + message.ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; + if !matches!(error_code, ParsedJsonValue::String(_)) + || !matches!(message, ParsedJsonValue::String(_)) + { + return Err( + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ); + } + if let Some(stacktrace) = stacktrace { + if !matches!(stacktrace, ParsedJsonValue::String(_)) { + return Err( + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ); + } + } + } + } + Ok(()) + } + + fn parse_value( + &mut self, + container_depth: usize, + ) -> Result { + match self.peek_byte() { + Some(b'{') => { + self.parse_object(container_depth)?; + Ok(ParsedJsonValue::Object) + } + Some(b'[') => { + self.parse_array(container_depth)?; + Ok(ParsedJsonValue::Array) + } + Some(b'"') => Ok(ParsedJsonValue::String(self.parse_string()?)), + Some(b'-' | b'0'..=b'9') => Ok(ParsedJsonValue::Number(self.parse_number()?)), + Some(b't') => { + self.parse_literal(b"true")?; + Ok(ParsedJsonValue::Boolean) + } + Some(b'f') => { + self.parse_literal(b"false")?; + Ok(ParsedJsonValue::Boolean) + } + Some(b'n') => { + self.parse_literal(b"null")?; + Ok(ParsedJsonValue::Null) + } + _ => Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson), + } + } + + fn parse_object( + &mut self, + depth: usize, + ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { + Self::require_depth(depth)?; + self.expect_byte(b'{')?; + self.skip_whitespace(); + if self.peek_byte() == Some(b'}') { + self.position += 1; + return Ok(()); + } + + loop { + self.parse_string()?; + self.skip_whitespace(); + self.expect_byte(b':')?; + self.skip_whitespace(); + self.parse_value(depth + 1)?; + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + } + Some(b'}') => { + self.position += 1; + return Ok(()); + } + _ => return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson), + } + } + } + + fn parse_array( + &mut self, + depth: usize, + ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { + Self::require_depth(depth)?; + self.expect_byte(b'[')?; + self.skip_whitespace(); + if self.peek_byte() == Some(b']') { + self.position += 1; + return Ok(()); + } + + loop { + self.parse_value(depth + 1)?; + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + } + Some(b']') => { + self.position += 1; + return Ok(()); + } + _ => return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson), + } + } + } + + fn require_depth(depth: usize) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { + if depth > MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH { + Err(WebDriverBiDiResponseEnvelopeParseError::JsonDepthExceeded) + } else { + Ok(()) + } + } + + fn parse_string(&mut self) -> Result, WebDriverBiDiResponseEnvelopeParseError> { + self.expect_byte(b'"')?; + let mut decoded = Vec::new(); + loop { + let byte = self + .peek_byte() + .ok_or(WebDriverBiDiResponseEnvelopeParseError::InvalidJson)?; + match byte { + b'"' => { + self.position += 1; + return Ok(decoded); + } + b'\\' => { + self.position += 1; + self.parse_escape(&mut decoded)?; + } + 0x00..=0x1f => { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + _ => { + decoded.push(byte); + self.position += 1; + } + } + } + } + + fn parse_escape( + &mut self, + decoded: &mut Vec, + ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { + let escaped = self + .peek_byte() + .ok_or(WebDriverBiDiResponseEnvelopeParseError::InvalidJson)?; + self.position += 1; + match escaped { + b'"' => decoded.push(b'"'), + b'\\' => decoded.push(b'\\'), + b'/' => decoded.push(b'/'), + b'b' => decoded.push(0x08), + b'f' => decoded.push(0x0c), + b'n' => decoded.push(b'\n'), + b'r' => decoded.push(b'\r'), + b't' => decoded.push(b'\t'), + b'u' => { + let scalar = self.parse_unicode_escape()?; + Self::push_utf8(decoded, scalar); + } + _ => return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson), + } + Ok(()) + } + + fn parse_unicode_escape(&mut self) -> Result { + let first = self.parse_hex_code_unit()?; + if (0xd800..=0xdbff).contains(&first) { + if self.peek_byte() != Some(b'\\') { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + self.position += 1; + if self.peek_byte() != Some(b'u') { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + self.position += 1; + let second = self.parse_hex_code_unit()?; + if !(0xdc00..=0xdfff).contains(&second) { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + return Ok( + 0x1_0000 + + ((u32::from(first) - 0xd800) << 10) + + (u32::from(second) - 0xdc00), + ); + } + if (0xdc00..=0xdfff).contains(&first) { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + Ok(u32::from(first)) + } + + fn push_utf8(decoded: &mut Vec, scalar: u32) { + if scalar <= 0x7f { + decoded.push(scalar as u8); + } else if scalar <= 0x7ff { + decoded.push((0xc0 | (scalar >> 6)) as u8); + decoded.push((0x80 | (scalar & 0x3f)) as u8); + } else if scalar <= 0xffff { + decoded.push((0xe0 | (scalar >> 12)) as u8); + decoded.push((0x80 | ((scalar >> 6) & 0x3f)) as u8); + decoded.push((0x80 | (scalar & 0x3f)) as u8); + } else { + decoded.push((0xf0 | (scalar >> 18)) as u8); + decoded.push((0x80 | ((scalar >> 12) & 0x3f)) as u8); + decoded.push((0x80 | ((scalar >> 6) & 0x3f)) as u8); + decoded.push((0x80 | (scalar & 0x3f)) as u8); + } + } + + fn parse_hex_code_unit(&mut self) -> Result { + let mut value = 0_u16; + for _ in 0..4 { + let byte = self + .peek_byte() + .ok_or(WebDriverBiDiResponseEnvelopeParseError::InvalidJson)?; + let digit = Self::hex_value(byte) + .ok_or(WebDriverBiDiResponseEnvelopeParseError::InvalidJson)?; + value = (value << 4) | u16::from(digit); + self.position += 1; + } + Ok(value) + } + + const fn hex_value(byte: u8) -> Option { + match byte { + b'0'..=b'9' => Some(byte - b'0'), + b'a'..=b'f' => Some(byte - b'a' + 10), + b'A'..=b'F' => Some(byte - b'A' + 10), + _ => None, + } + } + + fn parse_number(&mut self) -> Result { + let start = self.position; + if self.peek_byte() == Some(b'-') { + self.position += 1; + } + + match self.peek_byte() { + Some(b'0') => { + self.position += 1; + if matches!(self.peek_byte(), Some(b'0'..=b'9')) { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + } + Some(b'1'..=b'9') => self.consume_digits(), + _ => return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson), + } + + if self.peek_byte() == Some(b'.') { + self.position += 1; + if !matches!(self.peek_byte(), Some(b'0'..=b'9')) { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + self.consume_digits(); + } + + if matches!(self.peek_byte(), Some(b'e' | b'E')) { + self.position += 1; + if matches!(self.peek_byte(), Some(b'+' | b'-')) { + self.position += 1; + } + if !matches!(self.peek_byte(), Some(b'0'..=b'9')) { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + self.consume_digits(); + } + + Ok(self.input[start..self.position].to_owned()) + } + + fn consume_digits(&mut self) { + while matches!(self.peek_byte(), Some(b'0'..=b'9')) { + self.position += 1; + } + } + + fn parse_literal( + &mut self, + literal: &[u8], + ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { + let end = self.position + literal.len(); + if self.input.as_bytes().get(self.position..end) != Some(literal) { + return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); + } + self.position = end; + Ok(()) + } + + fn skip_whitespace(&mut self) { + while matches!(self.peek_byte(), Some(b' ' | b'\t' | b'\r' | b'\n')) { + self.position += 1; + } + } + + fn expect_byte(&mut self, expected: u8) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { + if self.peek_byte() == Some(expected) { + self.position += 1; + Ok(()) + } else { + Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson) + } + } + + fn peek_byte(&self) -> Option { + self.input.as_bytes().get(self.position).copied() + } +} From 9f924d261209874c09fa14c31b72d863bd701200 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:19:00 +0900 Subject: [PATCH 004/118] feat(core): expose BiDi response envelope parser --- crates/originweave-core/src/lib.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index fb43a48b9..c8cfd5290 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -33,6 +33,7 @@ mod browser_registry_coverage; mod contracts; mod webdriver_bidi_command; mod webdriver_bidi_response_document; +mod webdriver_bidi_response_envelope; mod webdriver_bidi_result; pub use browser_authority_registry::BrowserAuthorityRegistry; @@ -75,6 +76,10 @@ pub use webdriver_bidi_response_document::{ BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_RESPONSE_DOCUMENT_BYTES, WebDriverBiDiResponseDocumentAdmissionError, }; +pub use webdriver_bidi_response_envelope::{ + MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH, MAX_WEBDRIVER_BIDI_RESPONSE_TOP_LEVEL_FIELDS, + ParsedWebDriverBiDiCommandResponseEnvelope, WebDriverBiDiResponseEnvelopeParseError, +}; pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; From addb63f1c4374dfad18256875a4e77d4e6405399 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:22:29 +0900 Subject: [PATCH 005/118] style(core): apply canonical response parser formatting --- .../src/webdriver_bidi_response_envelope.rs | 29 ++++++++----------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs index 0dc2c5add..aba118bba 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs @@ -46,7 +46,9 @@ impl fmt::Display for WebDriverBiDiResponseEnvelopeParseError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { formatter.write_str(match self { Self::InvalidJson => "WebDriver BiDi response document is not valid JSON", - Self::JsonDepthExceeded => "WebDriver BiDi response JSON depth exceeds the safety budget", + Self::JsonDepthExceeded => { + "WebDriver BiDi response JSON depth exceeds the safety budget" + } Self::TopLevelFieldCountExceeded => { "WebDriver BiDi response top-level field count exceeds the safety budget" } @@ -147,9 +149,7 @@ impl<'input> ResponseEnvelopeParser<'input> { Self { input, position: 0 } } - fn parse( - mut self, - ) -> Result { + fn parse(mut self) -> Result { self.skip_whitespace(); self.expect_byte(b'{')?; self.skip_whitespace(); @@ -263,8 +263,8 @@ impl<'input> ResponseEnvelopeParser<'input> { ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { match kind { WebDriverBiDiCommandResponseKind::Success => { - let result = - result.ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; + let result = result + .ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; if !matches!(result, ParsedJsonValue::Object) { return Err( WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, @@ -274,8 +274,8 @@ impl<'input> ResponseEnvelopeParser<'input> { WebDriverBiDiCommandResponseKind::Error => { let error_code = error_code .ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; - let message = - message.ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; + let message = message + .ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; if !matches!(error_code, ParsedJsonValue::String(_)) || !matches!(message, ParsedJsonValue::String(_)) { @@ -359,10 +359,7 @@ impl<'input> ResponseEnvelopeParser<'input> { } } - fn parse_array( - &mut self, - depth: usize, - ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { + fn parse_array(&mut self, depth: usize) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { Self::require_depth(depth)?; self.expect_byte(b'[')?; self.skip_whitespace(); @@ -464,11 +461,9 @@ impl<'input> ResponseEnvelopeParser<'input> { if !(0xdc00..=0xdfff).contains(&second) { return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); } - return Ok( - 0x1_0000 - + ((u32::from(first) - 0xd800) << 10) - + (u32::from(second) - 0xdc00), - ); + return Ok(0x1_0000 + + ((u32::from(first) - 0xd800) << 10) + + (u32::from(second) - 0xdc00)); } if (0xdc00..=0xdfff).contains(&first) { return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); From eb086b09a7c58bd5fb1086e587d3cc3ec7ce1256 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:24:47 +0900 Subject: [PATCH 006/118] test(core): exercise hostile BiDi response JSON grammar --- ...ver_bidi_response_envelope_hostile_json.rs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_response_envelope_hostile_json.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_response_envelope_hostile_json.rs b/crates/originweave-core/tests/webdriver_bidi_response_envelope_hostile_json.rs new file mode 100644 index 000000000..5b56399be --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_response_envelope_hostile_json.rs @@ -0,0 +1,107 @@ +use std::error::Error; + +use originweave_core::{ + BoundedWebDriverBiDiResponseDocument, WebDriverBiDiResponseEnvelopeParseError, +}; + +fn assert_invalid_json(raw: &str) -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new(raw)?; + assert_eq!( + document.parse_command_response(), + Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson) + ); + Ok(()) +} + +#[test] +fn parser_rejects_top_level_separator_trailing_document_and_missing_colon_faults() +-> Result<(), Box> { + for raw in [ + "{\"type\":\"success\" \"id\":1,\"result\":{}}", + "{\"type\" \"success\",\"id\":1,\"result\":{}}", + "{\"type\":\"success\",\"id\":1,\"result\":{}} {}", + ] { + assert_invalid_json(raw)?; + } + + let empty = BoundedWebDriverBiDiResponseDocument::new("{}")?; + assert_eq!( + empty.parse_command_response(), + Err(WebDriverBiDiResponseEnvelopeParseError::MissingResponseType) + ); + Ok(()) +} + +#[test] +fn parser_rejects_malformed_nested_object_array_string_and_literal_values() +-> Result<(), Box> { + for raw in [ + "{\"type\":\"success\",\"id\":1,\"result\":{\"a\":1 \"b\":2}}", + "{\"type\":\"success\",\"id\":1,\"result\":{},\"x\":[1 2]}", + "{\"type\":\"success\",\"id\":1,\"result\":{},\"x\":tru}", + "{\"type\":\"success\",\"id\":1,\"result\":{},\"x\":-}", + "{\"type\":\"success\",\"id\":1,\"result\":{},\"x\":1.}", + "{\"type\":\"success\",\"id\":1,\"result\":{},\"x\":1e}", + "{\"type\":\"success\",\"id\":1,\"result\":{},\"x\":\"unterminated}", + ] { + assert_invalid_json(raw)?; + } + + let raw_control = "{\"type\":\"success\",\"id\":1,\"result\":{},\"x\":\"bad\u{0001}text\"}"; + assert_invalid_json(raw_control)?; + Ok(()) +} + +#[test] +fn parser_accepts_complete_json_escape_number_and_nested_container_forms() +-> Result<(), Box> { + let raw = concat!( + r#"{"type":"success","id":1,"result":{"a":1,"b":2},"esc":""#, + r#"\"\\\/\b\f\n\r\t","zero":0,"signed_exponent":1e+2,"nested":[1,2,{"ok":true}]}"#, + ); + let parsed = BoundedWebDriverBiDiResponseDocument::new(raw)?.parse_command_response()?; + assert_eq!(parsed.response_id(), Some(1)); + Ok(()) +} + +#[test] +fn parser_accepts_all_utf8_widths_from_json_unicode_escapes() -> Result<(), Box> { + for raw in [ + r#"{"type":"success","id":1,"result":{},"text":"\u0041"}"#, + r#"{"type":"success","id":1,"result":{},"text":"\u00E9"}"#, + r#"{"type":"success","id":1,"result":{},"text":"\u263A"}"#, + r#"{"type":"success","id":1,"result":{},"text":"\uD83D\uDE00"}"#, + r#"{"type":"success","id":1,"result":{},"text":"\u00AF"}"#, + ] { + let parsed = BoundedWebDriverBiDiResponseDocument::new(raw)?.parse_command_response()?; + assert_eq!(parsed.response_id(), Some(1)); + } + Ok(()) +} + +#[test] +fn parser_rejects_invalid_unicode_escape_sequences() -> Result<(), Box> { + for raw in [ + r#"{"type":"success","id":1,"result":{},"text":"\uD83D"}"#, + r#"{"type":"success","id":1,"result":{},"text":"\uD83D\x"}"#, + r#"{"type":"success","id":1,"result":{},"text":"\uD83D\u0041"}"#, + r#"{"type":"success","id":1,"result":{},"text":"\uDE00"}"#, + r#"{"type":"success","id":1,"result":{},"text":"\u12"}"#, + r#"{"type":"success","id":1,"result":{},"text":"\u00G0"}"#, + ] { + assert_invalid_json(raw)?; + } + Ok(()) +} + +#[test] +fn parser_rejects_integer_overflow_even_before_protocol_range_validation() +-> Result<(), Box> { + let raw = "{\"type\":\"success\",\"id\":18446744073709551616,\"result\":{}}"; + let document = BoundedWebDriverBiDiResponseDocument::new(raw)?; + assert_eq!( + document.parse_command_response(), + Err(WebDriverBiDiResponseEnvelopeParseError::InvalidResponseId) + ); + Ok(()) +} From 5c8df7608d3a1b985e361f1e7f69e99fc759ae30 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 11:09:05 +0900 Subject: [PATCH 007/118] fix(core): satisfy strict BiDi parser clippy --- .../src/webdriver_bidi_response_envelope.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs index aba118bba..a5d5b34ba 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs @@ -283,12 +283,12 @@ impl<'input> ResponseEnvelopeParser<'input> { WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, ); } - if let Some(stacktrace) = stacktrace { - if !matches!(stacktrace, ParsedJsonValue::String(_)) { - return Err( - WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, - ); - } + if let Some(stacktrace) = stacktrace + && !matches!(stacktrace, ParsedJsonValue::String(_)) + { + return Err( + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ); } } } From e455fe8846d36ee3ee7976ea1b945df250f891b6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:09:39 -0700 Subject: [PATCH 008/118] test(core): cover response parser failure edges --- ...er_bidi_response_envelope_failure_edges.rs | 65 +++++++++++++++++++ 1 file changed, 65 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs b/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs new file mode 100644 index 000000000..9eebae62e --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs @@ -0,0 +1,65 @@ +use std::error::Error; + +use originweave_core::{ + BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH, + WebDriverBiDiResponseEnvelopeParseError, +}; + +fn assert_invalid_json(raw: &str) -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new(raw)?; + assert_eq!( + document.parse_command_response(), + Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson) + ); + Ok(()) +} + +#[test] +fn parser_rejects_non_object_and_truncated_literal_documents() -> Result<(), Box> { + for raw in [ + "[]", + r#"{"type":"success","id":1,"result":{},"x":falsX}"#, + r#"{"type":"success","id":1,"result":{},"x":nulX}"#, + ] { + assert_invalid_json(raw)?; + } + Ok(()) +} + +#[test] +fn parser_rejects_truncated_escape_and_unicode_code_units() -> Result<(), Box> { + for raw in [ + r#"{"type":"success","id":1,"result":{},"x":"\"#, + r#"{"type":"success","id":1,"result":{},"x":"\u12"#, + r#"{"type":"success","id":1,"result":{},"x":"\uD83D\u"#, + ] { + assert_invalid_json(raw)?; + } + Ok(()) +} + +#[test] +fn parser_rejects_nested_object_key_and_colon_faults() -> Result<(), Box> { + for raw in [ + r#"{"type":"success","id":1,"result":{},"x":{1:2}}"#, + r#"{"type":"success","id":1,"result":{},"x":{"a" 1}}"#, + ] { + assert_invalid_json(raw)?; + } + Ok(()) +} + +#[test] +fn parser_enforces_depth_budget_for_object_nesting() -> Result<(), Box> { + let over_nested = format!( + "{{\"type\":\"success\",\"id\":1,\"result\":{{\"x\":{}{}}}}}", + "{\"k\":".repeat(MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH - 1), + "}".repeat(MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH - 1) + ); + let document = BoundedWebDriverBiDiResponseDocument::new(&over_nested)?; + assert_eq!( + document.parse_command_response(), + Err(WebDriverBiDiResponseEnvelopeParseError::JsonDepthExceeded) + ); + Ok(()) +} From 944787908619bb45df6a6430222d03be5c7357cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:12:04 -0700 Subject: [PATCH 009/118] fix(core): remove unreachable parser error edges --- .../src/webdriver_bidi_response_envelope.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs index a5d5b34ba..a8c4681b1 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs @@ -196,13 +196,17 @@ impl<'input> ResponseEnvelopeParser<'input> { self.position += 1; self.skip_whitespace(); } - Some(b'}') => break, + Some(b'}') => { + self.position += 1; + break; + } _ => return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson), } } + } else { + self.position += 1; } - self.expect_byte(b'}')?; self.skip_whitespace(); if self.position != self.input.len() { return Err(WebDriverBiDiResponseEnvelopeParseError::InvalidJson); @@ -331,7 +335,7 @@ impl<'input> ResponseEnvelopeParser<'input> { depth: usize, ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { Self::require_depth(depth)?; - self.expect_byte(b'{')?; + self.position += 1; self.skip_whitespace(); if self.peek_byte() == Some(b'}') { self.position += 1; @@ -361,7 +365,7 @@ impl<'input> ResponseEnvelopeParser<'input> { fn parse_array(&mut self, depth: usize) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { Self::require_depth(depth)?; - self.expect_byte(b'[')?; + self.position += 1; self.skip_whitespace(); if self.peek_byte() == Some(b']') { self.position += 1; From d45118b9ac39e04cb36ed8486211fe025001db34 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:09:59 -0700 Subject: [PATCH 010/118] test(core): respect bounded response admission boundary --- ...er_bidi_response_envelope_failure_edges.rs | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs b/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs index 9eebae62e..c2050b517 100644 --- a/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs +++ b/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs @@ -2,7 +2,7 @@ use std::error::Error; use originweave_core::{ BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH, - WebDriverBiDiResponseEnvelopeParseError, + WebDriverBiDiResponseDocumentAdmissionError, WebDriverBiDiResponseEnvelopeParseError, }; fn assert_invalid_json(raw: &str) -> Result<(), Box> { @@ -15,9 +15,17 @@ fn assert_invalid_json(raw: &str) -> Result<(), Box> { } #[test] -fn parser_rejects_non_object_and_truncated_literal_documents() -> Result<(), Box> { +fn non_object_response_stops_at_document_admission_before_parser() { + let error = BoundedWebDriverBiDiResponseDocument::new("[]").unwrap_err(); + assert_eq!( + error, + WebDriverBiDiResponseDocumentAdmissionError::InvalidObjectBoundary + ); +} + +#[test] +fn parser_rejects_truncated_literal_documents() -> Result<(), Box> { for raw in [ - "[]", r#"{"type":"success","id":1,"result":{},"x":falsX}"#, r#"{"type":"success","id":1,"result":{},"x":nulX}"#, ] { @@ -27,11 +35,11 @@ fn parser_rejects_non_object_and_truncated_literal_documents() -> Result<(), Box } #[test] -fn parser_rejects_truncated_escape_and_unicode_code_units() -> Result<(), Box> { +fn parser_rejects_malformed_escape_and_unicode_code_units() -> Result<(), Box> { for raw in [ - r#"{"type":"success","id":1,"result":{},"x":"\"#, - r#"{"type":"success","id":1,"result":{},"x":"\u12"#, - r#"{"type":"success","id":1,"result":{},"x":"\uD83D\u"#, + r#"{"type":"success","id":1,"result":{},"x":"\q"}"#, + r#"{"type":"success","id":1,"result":{},"x":"\u12G4"}"#, + r#"{"type":"success","id":1,"result":{},"x":"\uD83D\u12G4"}"#, ] { assert_invalid_json(raw)?; } From 1b157efe48d82d68d2f3bb25a10975a3140734d9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:13:05 -0700 Subject: [PATCH 011/118] test(core): avoid panic-prone admission assertion --- .../webdriver_bidi_response_envelope_failure_edges.rs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs b/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs index c2050b517..692e7ea68 100644 --- a/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs +++ b/crates/originweave-core/tests/webdriver_bidi_response_envelope_failure_edges.rs @@ -16,11 +16,10 @@ fn assert_invalid_json(raw: &str) -> Result<(), Box> { #[test] fn non_object_response_stops_at_document_admission_before_parser() { - let error = BoundedWebDriverBiDiResponseDocument::new("[]").unwrap_err(); - assert_eq!( - error, - WebDriverBiDiResponseDocumentAdmissionError::InvalidObjectBoundary - ); + assert!(matches!( + BoundedWebDriverBiDiResponseDocument::new("[]"), + Err(WebDriverBiDiResponseDocumentAdmissionError::InvalidObjectBoundary) + )); } #[test] From 67f5ca317d1985c90c2402403105f4c005053fdc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:17:48 -0700 Subject: [PATCH 012/118] fix(core): remove unreachable parser coverage branches --- .../src/webdriver_bidi_response_envelope.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs index a8c4681b1..db3d2b846 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs @@ -151,7 +151,8 @@ impl<'input> ResponseEnvelopeParser<'input> { fn parse(mut self) -> Result { self.skip_whitespace(); - self.expect_byte(b'{')?; + // The bounded-document constructor proves the first non-whitespace byte is `{`. + self.position += 1; self.skip_whitespace(); let mut seen_fields: Vec> = Vec::new(); @@ -428,9 +429,9 @@ impl<'input> ResponseEnvelopeParser<'input> { &mut self, decoded: &mut Vec, ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { - let escaped = self - .peek_byte() - .ok_or(WebDriverBiDiResponseEnvelopeParseError::InvalidJson)?; + // The bounded top-level object guarantees a following byte; map any violated internal + // invariant to the existing fail-closed invalid-JSON path without a second unreachable branch. + let escaped = self.peek_byte().unwrap_or_default(); self.position += 1; match escaped { b'"' => decoded.push(b'"'), @@ -496,9 +497,9 @@ impl<'input> ResponseEnvelopeParser<'input> { fn parse_hex_code_unit(&mut self) -> Result { let mut value = 0_u16; for _ in 0..4 { - let byte = self - .peek_byte() - .ok_or(WebDriverBiDiResponseEnvelopeParseError::InvalidJson)?; + // A truncated escape cannot run past the admitted top-level closing `}`. If an + // internal invariant is ever violated, zero still deterministically fails hex decoding. + let byte = self.peek_byte().unwrap_or_default(); let digit = Self::hex_value(byte) .ok_or(WebDriverBiDiResponseEnvelopeParseError::InvalidJson)?; value = (value << 4) | u16::from(digit); From e65df259da94082b9475e300f2854c820ae02d04 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 23:17:45 -0700 Subject: [PATCH 013/118] test(core): require parsed BiDi response correlation --- ...ver_bidi_locate_nodes_response_document.rs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_locate_nodes_response_document.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_response_document.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_response_document.rs new file mode 100644 index 000000000..a23ae5f26 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_response_document.rs @@ -0,0 +1,100 @@ +use std::error::Error; + +use originweave_core::{ + BoundedWebDriverBiDiResponseDocument, WebDriverBiDiAccessibilityQuery, + WebDriverBiDiCommandResponseKind, WebDriverBiDiLocateNodesCommand, + WebDriverBiDiLocateNodesResponseCorrelationError, + WebDriverBiDiLocateNodesResponseDocumentError, WebDriverBiDiLocateNodesResponseEnvelopeError, + WebDriverBiDiResponseEnvelopeParseError, +}; + +fn locate_nodes_command(command_id: u64) -> Result> { + let query = WebDriverBiDiAccessibilityQuery::new(Some("button"), Some("Submit task"), 4)?; + Ok(WebDriverBiDiLocateNodesCommand::new( + command_id, + "context-a", + &query, + )?) +} + +#[test] +fn bounded_success_document_is_parsed_and_correlated_in_one_consuming_boundary() +-> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{"nodes":[]}}"#, + )?; + let correlated = locate_nodes_command(42)?.correlate_response_document(document)?; + + assert_eq!(correlated.kind(), WebDriverBiDiCommandResponseKind::Success); + assert_eq!(correlated.command_id(), 42); + assert_eq!(correlated.browsing_context(), "context-a"); + Ok(()) +} + +#[test] +fn malformed_bounded_document_preserves_the_parser_failure() -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{},}"#, + )?; + let result = locate_nodes_command(42)?.correlate_response_document(document); + + assert_eq!( + result, + Err(WebDriverBiDiLocateNodesResponseDocumentError::Parse( + WebDriverBiDiResponseEnvelopeParseError::InvalidJson, + )) + ); + Ok(()) +} + +#[test] +fn parsed_response_id_mismatch_preserves_exact_correlation_failure() -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":41,"result":{}}"#, + )?; + let result = locate_nodes_command(42)?.correlate_response_document(document); + + assert_eq!( + result, + Err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope( + WebDriverBiDiLocateNodesResponseEnvelopeError::Correlation( + WebDriverBiDiLocateNodesResponseCorrelationError::ResponseIdMismatch { + expected: 42, + actual: 41, + }, + ), + )) + ); + Ok(()) +} + +#[test] +fn nullable_error_document_remains_explicitly_uncorrelatable() -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"error","id":null,"error":"invalid argument","message":"bad request"}"#, + )?; + let result = locate_nodes_command(42)?.correlate_response_document(document); + + assert_eq!( + result, + Err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope( + WebDriverBiDiLocateNodesResponseEnvelopeError::UncorrelatableErrorResponse, + )) + ); + Ok(()) +} + +#[test] +fn document_correlation_error_preserves_nested_error_sources() { + let parse = WebDriverBiDiLocateNodesResponseDocumentError::Parse( + WebDriverBiDiResponseEnvelopeParseError::InvalidJson, + ); + assert!(parse.source().is_some()); + assert!(!parse.to_string().is_empty()); + + let envelope = WebDriverBiDiLocateNodesResponseDocumentError::Envelope( + WebDriverBiDiLocateNodesResponseEnvelopeError::MissingResponseId, + ); + assert!(envelope.source().is_some()); + assert!(!envelope.to_string().is_empty()); +} From e9d90dfa73552a8d0dd4e1d022478864b3300167 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 23:18:50 -0700 Subject: [PATCH 014/118] test(core): apply canonical BiDi correlation formatting --- ...ebdriver_bidi_locate_nodes_response_document.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_response_document.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_response_document.rs index a23ae5f26..811def1f3 100644 --- a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_response_document.rs +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_response_document.rs @@ -8,7 +8,9 @@ use originweave_core::{ WebDriverBiDiResponseEnvelopeParseError, }; -fn locate_nodes_command(command_id: u64) -> Result> { +fn locate_nodes_command( + command_id: u64, +) -> Result> { let query = WebDriverBiDiAccessibilityQuery::new(Some("button"), Some("Submit task"), 4)?; Ok(WebDriverBiDiLocateNodesCommand::new( command_id, @@ -33,9 +35,8 @@ fn bounded_success_document_is_parsed_and_correlated_in_one_consuming_boundary() #[test] fn malformed_bounded_document_preserves_the_parser_failure() -> Result<(), Box> { - let document = BoundedWebDriverBiDiResponseDocument::new( - r#"{"type":"success","id":42,"result":{},}"#, - )?; + let document = + BoundedWebDriverBiDiResponseDocument::new(r#"{"type":"success","id":42,"result":{},}"#)?; let result = locate_nodes_command(42)?.correlate_response_document(document); assert_eq!( @@ -49,9 +50,8 @@ fn malformed_bounded_document_preserves_the_parser_failure() -> Result<(), Box Result<(), Box> { - let document = BoundedWebDriverBiDiResponseDocument::new( - r#"{"type":"success","id":41,"result":{}}"#, - )?; + let document = + BoundedWebDriverBiDiResponseDocument::new(r#"{"type":"success","id":41,"result":{}}"#)?; let result = locate_nodes_command(42)?.correlate_response_document(document); assert_eq!( From 5481255ee62f9ab5ba936cb87373f416f5456fbc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 23:34:47 -0700 Subject: [PATCH 015/118] feat(core): correlate bounded BiDi response documents --- crates/originweave-core/src/lib.rs | 2 + ...iver_bidi_response_document_correlation.rs | 66 +++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index c8cfd5290..dc9ce4467 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -33,6 +33,7 @@ mod browser_registry_coverage; mod contracts; mod webdriver_bidi_command; mod webdriver_bidi_response_document; +mod webdriver_bidi_response_document_correlation; mod webdriver_bidi_response_envelope; mod webdriver_bidi_result; @@ -76,6 +77,7 @@ pub use webdriver_bidi_response_document::{ BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_RESPONSE_DOCUMENT_BYTES, WebDriverBiDiResponseDocumentAdmissionError, }; +pub use webdriver_bidi_response_document_correlation::WebDriverBiDiLocateNodesResponseDocumentError; pub use webdriver_bidi_response_envelope::{ MAX_WEBDRIVER_BIDI_RESPONSE_JSON_DEPTH, MAX_WEBDRIVER_BIDI_RESPONSE_TOP_LEVEL_FIELDS, ParsedWebDriverBiDiCommandResponseEnvelope, WebDriverBiDiResponseEnvelopeParseError, diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs new file mode 100644 index 000000000..c0dd26b08 --- /dev/null +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -0,0 +1,66 @@ +use std::error::Error; +use std::fmt::{Display, Formatter}; + +use crate::webdriver_bidi_command::{ + CorrelatedWebDriverBiDiLocateNodesResponse, WebDriverBiDiLocateNodesCommand, + WebDriverBiDiLocateNodesResponseEnvelopeError, +}; +use crate::webdriver_bidi_response_document::BoundedWebDriverBiDiResponseDocument; +use crate::webdriver_bidi_response_envelope::WebDriverBiDiResponseEnvelopeParseError; + +/// Fail-closed errors while parsing and correlating one bounded WebDriver BiDi response document. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiLocateNodesResponseDocumentError { + /// The bounded document failed complete WebDriver BiDi response-envelope parsing. + Parse(WebDriverBiDiResponseEnvelopeParseError), + /// The parsed envelope failed exact command correlation. + Envelope(WebDriverBiDiLocateNodesResponseEnvelopeError), +} + +impl Display for WebDriverBiDiLocateNodesResponseDocumentError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Parse(error) => write!( + formatter, + "WebDriver BiDi response document rejected envelope parsing: {error}" + ), + Self::Envelope(error) => write!( + formatter, + "WebDriver BiDi response document rejected command correlation: {error}" + ), + } + } +} + +impl Error for WebDriverBiDiLocateNodesResponseDocumentError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Parse(error) => Some(error), + Self::Envelope(error) => Some(error), + } + } +} + +impl WebDriverBiDiLocateNodesCommand { + /// Consume this command and one bounded raw response through parsing and exact correlation. + /// + /// The document must first pass complete response-envelope parsing. Only the resulting typed + /// response kind and protocol-range response id are then admitted to the existing exact command + /// correlation boundary. Parser and correlation failures remain distinguishable and preserve + /// their causal error sources. This boundary does not authenticate Chromium, ChromeDriver, or + /// WebSocket transport provenance, validate `locateNodes` result nodes, mint node authority, + /// authorize an Agent action, execute browser input, or prove a post-condition. + pub fn correlate_response_document( + self, + document: BoundedWebDriverBiDiResponseDocument, + ) -> Result< + CorrelatedWebDriverBiDiLocateNodesResponse, + WebDriverBiDiLocateNodesResponseDocumentError, + > { + let parsed = document + .parse_command_response() + .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Parse)?; + self.correlate_response_envelope(parsed.kind(), parsed.response_id()) + .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope) + } +} From 35a9f4290adfafed4f6016e1453bc66aad0b0906 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 23:39:06 -0700 Subject: [PATCH 016/118] docs(changelog): record bounded BiDi document correlation --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8afb9ec7c..f87011269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Fail-closed WebDriver BiDi `locateNodes` response correlation that consumes the exact serialized command, rejects out-of-range or mismatched response `id` values, and returns non-cloneable correlation evidence carrying only the matched command identifier and browsing context without parsing the response or granting browser/Agent authority. - Typed WebDriver BiDi response-envelope correlation that preserves success/error classification, rejects success responses with absent ids, treats nullable error ids as explicitly uncorrelatable, and prevents correlated error envelopes from becoming success correlation evidence without raw JSON parsing or browser/Agent authority grants. - Bounded raw WebDriver BiDi response-document admission before JSON parsing, with a 65,536-byte product safety budget, exact wire-text retention, JSON-whitespace-aware top-level object-boundary checks, and typed fail-closed errors; this coarse boundary deliberately does not claim JSON validity, response correlation, browser authenticity, or Agent authority. +- One consuming bounded WebDriver BiDi response-document correlation boundary that parses the exact admitted JSON document and feeds only its typed response kind and protocol-range id into the existing exact command-correlation gate, preserving nested parser/correlation error sources without authenticating transport or granting browser/Agent authority. - Fail-closed WebDriver BiDi `script.NodeRemoteValue` admission that requires the exact remote type `node` and a non-empty `sharedId` within the same UTF-8 identifier budget as browser session and context identifiers, rejecting control and whitespace so an untrusted `locateNodes` item cannot be retained as a later typed-input handle without a usable shared node identity. - Same-call `locateNodes` result admission that revalidates the exact current session, browsing context, canonical origin, and document epoch, rejects an over-budget or non-node result, and translates each admitted `sharedId` through the authority registry into an `ObservedNodeHandle` without performing browser I/O. - Same-call QueryNodes admission that transfers a non-cloneable SemanticObservation protocol-use proof by ownership into `bind_current_nodes` before an untrusted `locateNodes` result can become current `ObservedNodeHandle` values, so Navigation-only or TypedInput-only proofs cannot mint observation handles. From e0b502ae8492d680babffdae19f1ca955051f843 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:08:52 -0700 Subject: [PATCH 017/118] test(core): reject unknown WebDriver BiDi error codes --- .../webdriver_bidi_response_error_code.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_response_error_code.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs new file mode 100644 index 000000000..3f8ee520d --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs @@ -0,0 +1,58 @@ +use std::error::Error; + +use originweave_core::BoundedWebDriverBiDiResponseDocument; + +const CURRENT_WEBDRIVER_BIDI_ERROR_CODES: &[&str] = &[ + "invalid argument", + "invalid selector", + "invalid session id", + "invalid web extension", + "move target out of bounds", + "no such alert", + "no such network collector", + "no such element", + "no such frame", + "no such handle", + "no such history entry", + "no such intercept", + "no such network data", + "no such node", + "no such request", + "no such screencast", + "no such script", + "no such storage partition", + "no such user context", + "no such web extension", + "session not created", + "unable to capture screen", + "unable to close browser", + "unable to set cookie", + "unable to set file input", + "unavailable network data", + "underspecified storage partition", + "unknown command", + "unknown error", + "unsupported operation", +]; + +#[test] +fn parser_accepts_current_webdriver_bidi_error_code_vocabulary() -> Result<(), Box> { + for error_code in CURRENT_WEBDRIVER_BIDI_ERROR_CODES { + let raw = format!( + "{{\"type\":\"error\",\"id\":7,\"error\":\"{error_code}\",\"message\":\"browser rejected command\"}}" + ); + let parsed = BoundedWebDriverBiDiResponseDocument::new(&raw)?.parse_command_response(); + assert!(parsed.is_ok(), "current WebDriver BiDi error code must remain admissible: {error_code}"); + } + Ok(()) +} + +#[test] +fn parser_rejects_unknown_webdriver_bidi_error_code() -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + "{\"type\":\"error\",\"id\":7,\"error\":\"made up browser failure\",\"message\":\"untrusted adapter text\"}", + )?; + + assert!(document.parse_command_response().is_err()); + Ok(()) +} From ebc127c8372de73d95922f589d3bd58e52cfe099 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:11:44 -0700 Subject: [PATCH 018/118] test(core): format WebDriver BiDi error-code RED --- .../tests/webdriver_bidi_response_error_code.rs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs index 3f8ee520d..5d4602647 100644 --- a/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs +++ b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs @@ -42,7 +42,10 @@ fn parser_accepts_current_webdriver_bidi_error_code_vocabulary() -> Result<(), B "{{\"type\":\"error\",\"id\":7,\"error\":\"{error_code}\",\"message\":\"browser rejected command\"}}" ); let parsed = BoundedWebDriverBiDiResponseDocument::new(&raw)?.parse_command_response(); - assert!(parsed.is_ok(), "current WebDriver BiDi error code must remain admissible: {error_code}"); + assert!( + parsed.is_ok(), + "current WebDriver BiDi error code must remain admissible: {error_code}" + ); } Ok(()) } @@ -53,6 +56,10 @@ fn parser_rejects_unknown_webdriver_bidi_error_code() -> Result<(), Box Date: Tue, 18 Aug 2026 00:16:23 -0700 Subject: [PATCH 019/118] fix(core): define WebDriver BiDi error-code vocabulary --- .../src/webdriver_bidi_error_code.rs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 crates/originweave-core/src/webdriver_bidi_error_code.rs diff --git a/crates/originweave-core/src/webdriver_bidi_error_code.rs b/crates/originweave-core/src/webdriver_bidi_error_code.rs new file mode 100644 index 000000000..ac7b24703 --- /dev/null +++ b/crates/originweave-core/src/webdriver_bidi_error_code.rs @@ -0,0 +1,37 @@ +/// Returns whether `value` is one of the error codes admitted by the current WebDriver BiDi specification. +pub(crate) fn is_webdriver_bidi_error_code(value: &[u8]) -> bool { + const ERROR_CODES: &[&[u8]] = &[ + b"invalid argument", + b"invalid selector", + b"invalid session id", + b"invalid web extension", + b"move target out of bounds", + b"no such alert", + b"no such network collector", + b"no such element", + b"no such frame", + b"no such handle", + b"no such history entry", + b"no such intercept", + b"no such network data", + b"no such node", + b"no such request", + b"no such screencast", + b"no such script", + b"no such storage partition", + b"no such user context", + b"no such web extension", + b"session not created", + b"unable to capture screen", + b"unable to close browser", + b"unable to set cookie", + b"unable to set file input", + b"unavailable network data", + b"underspecified storage partition", + b"unknown command", + b"unknown error", + b"unsupported operation", + ]; + + ERROR_CODES.contains(&value) +} From 9b85be515cc2159439c95774627642b3210283e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:16:46 -0700 Subject: [PATCH 020/118] fix(core): wire WebDriver BiDi error-code validator --- crates/originweave-core/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index dc9ce4467..8571f9d87 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -32,6 +32,7 @@ mod browser_registry; mod browser_registry_coverage; mod contracts; mod webdriver_bidi_command; +mod webdriver_bidi_error_code; mod webdriver_bidi_response_document; mod webdriver_bidi_response_document_correlation; mod webdriver_bidi_response_envelope; From e8159640d3c36b07838b37400aab0ddc90e810be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:20:11 -0700 Subject: [PATCH 021/118] fix(core): reject unknown WebDriver BiDi error codes --- .../src/webdriver_bidi_response_envelope.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs index db3d2b846..066acce68 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs @@ -1,6 +1,7 @@ use std::{error::Error, fmt}; use crate::{ + webdriver_bidi_error_code::is_webdriver_bidi_error_code, BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_COMMAND_ID, WebDriverBiDiCommandResponseKind, }; @@ -40,6 +41,8 @@ pub enum WebDriverBiDiResponseEnvelopeParseError { MissingRequiredPayload, /// A required response payload field has the wrong JSON value type. InvalidRequiredPayloadType, + /// The error response uses a string outside the current WebDriver BiDi `ErrorCode` vocabulary. + UnexpectedErrorCode, } impl fmt::Display for WebDriverBiDiResponseEnvelopeParseError { @@ -65,6 +68,7 @@ impl fmt::Display for WebDriverBiDiResponseEnvelopeParseError { Self::InvalidRequiredPayloadType => { "WebDriver BiDi response payload field has an invalid JSON type" } + Self::UnexpectedErrorCode => "WebDriver BiDi response error code is not recognized", }) } } @@ -281,12 +285,18 @@ impl<'input> ResponseEnvelopeParser<'input> { .ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; let message = message .ok_or(WebDriverBiDiResponseEnvelopeParseError::MissingRequiredPayload)?; - if !matches!(error_code, ParsedJsonValue::String(_)) - || !matches!(message, ParsedJsonValue::String(_)) - { + let ParsedJsonValue::String(error_code) = error_code else { return Err( WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, ); + }; + if !matches!(message, ParsedJsonValue::String(_)) { + return Err( + WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, + ); + } + if !is_webdriver_bidi_error_code(&error_code) { + return Err(WebDriverBiDiResponseEnvelopeParseError::UnexpectedErrorCode); } if let Some(stacktrace) = stacktrace && !matches!(stacktrace, ParsedJsonValue::String(_)) From 5b829fe90da6712bb3fbdd890ea5a18325288f32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:22:43 -0700 Subject: [PATCH 022/118] style(core): apply canonical rustfmt ordering --- .../originweave-core/src/webdriver_bidi_response_envelope.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs index 066acce68..29e3e6728 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs @@ -1,9 +1,8 @@ use std::{error::Error, fmt}; use crate::{ - webdriver_bidi_error_code::is_webdriver_bidi_error_code, BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_COMMAND_ID, - WebDriverBiDiCommandResponseKind, + WebDriverBiDiCommandResponseKind, webdriver_bidi_error_code::is_webdriver_bidi_error_code, }; /// Maximum accepted JSON container nesting depth for one WebDriver BiDi response document. From f5e087d58f5540993946c5f4057bef3f1623def9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:01:51 -0700 Subject: [PATCH 023/118] test(core): cover current BiDi client-window error --- .../originweave-core/tests/webdriver_bidi_response_error_code.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs index 5d4602647..3cc7785b1 100644 --- a/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs +++ b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs @@ -9,6 +9,7 @@ const CURRENT_WEBDRIVER_BIDI_ERROR_CODES: &[&str] = &[ "invalid web extension", "move target out of bounds", "no such alert", + "no such client window", "no such network collector", "no such element", "no such frame", From 4077a79a49d77cb929a3818371b33c6e90cc7f56 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:04:21 -0700 Subject: [PATCH 024/118] fix(core): admit current BiDi client-window error --- crates/originweave-core/src/webdriver_bidi_error_code.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/src/webdriver_bidi_error_code.rs b/crates/originweave-core/src/webdriver_bidi_error_code.rs index ac7b24703..ffcea6a1b 100644 --- a/crates/originweave-core/src/webdriver_bidi_error_code.rs +++ b/crates/originweave-core/src/webdriver_bidi_error_code.rs @@ -7,6 +7,7 @@ pub(crate) fn is_webdriver_bidi_error_code(value: &[u8]) -> bool { b"invalid web extension", b"move target out of bounds", b"no such alert", + b"no such client window", b"no such network collector", b"no such element", b"no such frame", From b7e5ba55090f22fefefd89bab638858ca42610d1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:06:27 -0700 Subject: [PATCH 025/118] test(core): keep BiDi failure regression panic-free --- .../tests/webdriver_bidi_response_error_code.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs index 3cc7785b1..faed05b61 100644 --- a/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs +++ b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs @@ -57,9 +57,15 @@ fn parser_rejects_unknown_webdriver_bidi_error_code() -> Result<(), Box { + return Err(std::io::Error::other( + "unknown WebDriver BiDi error code was unexpectedly accepted", + ) + .into()); + } + Err(error) => error, + }; assert!(!error.to_string().is_empty()); assert!(error.source().is_none()); Ok(()) From 9d39c5907c8d983d8c446f6f1df9654990576c60 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:11:02 -0700 Subject: [PATCH 026/118] docs(doctoring): record current BiDi error-code contract --- docs/doctoring/browser-agent-protocols.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index 9d35587d3..dbf3ef731 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -14,6 +14,8 @@ For the bounded `browsingContext.locateNodes` command-serialization boundary, th WebDriver BiDi commands may execute concurrently and finish out of order. The Editor’s Draft defines the command id as the local end’s correlation identifier and sets a successful `CommandResponse.id` to that exact command id; an `ErrorResponse.id` may be `null` when no valid command id can be recovered. OriginWeave therefore fails closed unless a non-null protocol-range response id exactly matches the consumed command before later payload admission. Parsing success/error envelopes, handling nullable malformed-command errors, and authenticating the browser transport remain separate adapter boundaries. +The same reviewed Editor’s Draft defines a closed `ErrorCode` vocabulary that currently includes `no such client window`. OriginWeave admits only the reviewed vocabulary at its bounded response-envelope parser and rejects unknown error-code text fail closed; adding a newly reviewed protocol code changes compatibility only and grants no browser, transport, node, policy, or Agent authority. + Primary sources: World Wide Web Consortium, *WebDriver BiDi* (published Working Draft and current Editor’s Draft). ## Chrome Manifest V3 From 0e8414c7c159c23d8dd1d6800cbd4d0199a524c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:35:10 -0700 Subject: [PATCH 027/118] test(core): require wire-derived locateNodes result admission --- ...webdriver_bidi_locate_nodes_wire_result.rs | 127 ++++++++++++++++++ 1 file changed, 127 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs new file mode 100644 index 000000000..1f91c02cb --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs @@ -0,0 +1,127 @@ +use std::error::Error; + +use originweave_core::{ + BoundedWebDriverBiDiResponseDocument, WebDriverBiDiAccessibilityQuery, + WebDriverBiDiLocateNodesCommand, +}; + +fn locate_nodes_command( + command_id: u64, + max_node_count: u16, +) -> Result> { + let query = WebDriverBiDiAccessibilityQuery::new( + Some("button"), + Some("Submit task"), + max_node_count, + )?; + Ok(WebDriverBiDiLocateNodesCommand::new( + command_id, + "context-a", + &query, + )?) +} + +#[test] +fn bounded_locate_nodes_document_admits_exact_wire_nodes_without_caller_selected_payload() +-> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node","sharedId":"node-a"},{"type":"node","sharedId":"node-b","value":{"nodeType":1}}]}}"#, + )?; + let admitted = locate_nodes_command(42, 2)?.admit_response_document_nodes(document)?; + + assert_eq!(admitted.command_id(), 42); + assert_eq!(admitted.browsing_context(), "context-a"); + assert_eq!(admitted.max_node_count(), 2); + assert_eq!(admitted.nodes().len(), 2); + assert_eq!(admitted.nodes()[0].remote_type(), "node"); + assert_eq!(admitted.nodes()[0].shared_id(), "node-a"); + assert_eq!(admitted.nodes()[1].shared_id(), "node-b"); + Ok(()) +} + +#[test] +fn wire_result_preserves_exact_command_node_budget() -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node","sharedId":"node-a"},{"type":"node","sharedId":"node-b"}]}}"#, + )?; + + assert!( + locate_nodes_command(42, 1)? + .admit_response_document_nodes(document) + .is_err() + ); + Ok(()) +} + +#[test] +fn wire_result_rejects_missing_shared_id() -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node"}]}}"#, + )?; + + assert!( + locate_nodes_command(42, 1)? + .admit_response_document_nodes(document) + .is_err() + ); + Ok(()) +} + +#[test] +fn wire_result_rejects_non_node_remote_value() -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{"nodes":[{"type":"window","sharedId":"node-a"}]}}"#, + )?; + + assert!( + locate_nodes_command(42, 1)? + .admit_response_document_nodes(document) + .is_err() + ); + Ok(()) +} + +#[test] +fn wire_result_requires_nodes_array() -> Result<(), Box> { + for raw in [ + r#"{"type":"success","id":42,"result":{}}"#, + r#"{"type":"success","id":42,"result":{"nodes":{}}}"#, + ] { + let document = BoundedWebDriverBiDiResponseDocument::new(raw)?; + assert!( + locate_nodes_command(42, 1)? + .admit_response_document_nodes(document) + .is_err() + ); + } + Ok(()) +} + +#[test] +fn wire_result_rejects_ambiguous_duplicate_result_or_node_fields() -> Result<(), Box> { + for raw in [ + r#"{"type":"success","id":42,"result":{"nodes":[],"nodes":[]}}"#, + r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node","type":"node","sharedId":"node-a"}]}}"#, + r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node","sharedId":"node-a","sharedId":"node-a"}]}}"#, + ] { + let document = BoundedWebDriverBiDiResponseDocument::new(raw)?; + assert!( + locate_nodes_command(42, 1)? + .admit_response_document_nodes(document) + .is_err() + ); + } + Ok(()) +} + +#[test] +fn wire_result_decodes_json_escaped_protocol_fields_before_admission() -> Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"res\u0075lt":{"no\u0064es":[{"ty\u0070e":"no\u0064e","shared\u0049d":"node-\u03b1"}]}}"#, + )?; + let admitted = locate_nodes_command(42, 1)?.admit_response_document_nodes(document)?; + + assert_eq!(admitted.nodes().len(), 1); + assert_eq!(admitted.nodes()[0].shared_id(), "node-α"); + Ok(()) +} From 3ad1c5fb34a21aafac188e4084e2ff9e2bfcf75c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:36:12 -0700 Subject: [PATCH 028/118] style(core): apply canonical wire-result test formatting --- .../tests/webdriver_bidi_locate_nodes_wire_result.rs | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs index 1f91c02cb..349d7912f 100644 --- a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs @@ -9,11 +9,8 @@ fn locate_nodes_command( command_id: u64, max_node_count: u16, ) -> Result> { - let query = WebDriverBiDiAccessibilityQuery::new( - Some("button"), - Some("Submit task"), - max_node_count, - )?; + let query = + WebDriverBiDiAccessibilityQuery::new(Some("button"), Some("Submit task"), max_node_count)?; Ok(WebDriverBiDiLocateNodesCommand::new( command_id, "context-a", @@ -115,7 +112,8 @@ fn wire_result_rejects_ambiguous_duplicate_result_or_node_fields() -> Result<(), } #[test] -fn wire_result_decodes_json_escaped_protocol_fields_before_admission() -> Result<(), Box> { +fn wire_result_decodes_json_escaped_protocol_fields_before_admission() -> Result<(), Box> +{ let document = BoundedWebDriverBiDiResponseDocument::new( r#"{"type":"success","id":42,"res\u0075lt":{"no\u0064es":[{"ty\u0070e":"no\u0064e","shared\u0049d":"node-\u03b1"}]}}"#, )?; From ee545e6650703c27a464d32c4c68b2cd6e79b289 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:39:38 -0700 Subject: [PATCH 029/118] feat(core): admit locateNodes nodes from bounded wire response --- ...iver_bidi_response_document_correlation.rs | 104 +++- .../locate_nodes_result_document.rs | 455 ++++++++++++++++++ 2 files changed, 557 insertions(+), 2 deletions(-) create mode 100644 crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index c0dd26b08..570ec9ddc 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -1,20 +1,46 @@ use std::error::Error; use std::fmt::{Display, Formatter}; +mod locate_nodes_result_document; + use crate::webdriver_bidi_command::{ CorrelatedWebDriverBiDiLocateNodesResponse, WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesResponseEnvelopeError, }; use crate::webdriver_bidi_response_document::BoundedWebDriverBiDiResponseDocument; use crate::webdriver_bidi_response_envelope::WebDriverBiDiResponseEnvelopeParseError; +use crate::webdriver_bidi_result::{ + ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, +}; -/// Fail-closed errors while parsing and correlating one bounded WebDriver BiDi response document. +/// Fail-closed errors while parsing, correlating, and admitting one bounded WebDriver BiDi +/// `locateNodes` response document. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WebDriverBiDiLocateNodesResponseDocumentError { /// The bounded document failed complete WebDriver BiDi response-envelope parsing. Parse(WebDriverBiDiResponseEnvelopeParseError), - /// The parsed envelope failed exact command correlation. + /// The parsed envelope failed exact command correlation or success-only conversion. Envelope(WebDriverBiDiLocateNodesResponseEnvelopeError), + /// The correlated success result omitted its required `nodes` field. + MissingResultNodes, + /// The correlated success result's `nodes` field was not a JSON array. + InvalidResultNodes, + /// The correlated success result repeated the decoded `nodes` field. + DuplicateResultNodes, + /// One `nodes` array item was not a JSON object. + InvalidResultNode, + /// One node object repeated decoded `type` or `sharedId` authority-relevant metadata. + DuplicateResultNodeField, + /// One node object omitted its required WebDriver BiDi remote-value `type` field. + MissingResultNodeType, + /// One node object's `type` field was not a JSON string. + InvalidResultNodeType, + /// One present node `sharedId` field was not a JSON string. + InvalidResultNodeSharedId, + /// Exact command-budget or remote-node admission rejected the wire-derived node batch. + ResultAdmission(WebDriverBiDiLocateNodesResultAdmissionError), + /// A second-pass result parser invariant failed after complete envelope parsing succeeded. + ResultParserInvariant, } impl Display for WebDriverBiDiLocateNodesResponseDocumentError { @@ -28,6 +54,32 @@ impl Display for WebDriverBiDiLocateNodesResponseDocumentError { formatter, "WebDriver BiDi response document rejected command correlation: {error}" ), + Self::MissingResultNodes => { + formatter.write_str("WebDriver BiDi locateNodes result is missing its nodes field") + } + Self::InvalidResultNodes => formatter + .write_str("WebDriver BiDi locateNodes result nodes field is not a JSON array"), + Self::DuplicateResultNodes => formatter.write_str( + "WebDriver BiDi locateNodes result contains duplicate decoded nodes fields", + ), + Self::InvalidResultNode => formatter + .write_str("WebDriver BiDi locateNodes result contains a non-object node item"), + Self::DuplicateResultNodeField => formatter.write_str( + "WebDriver BiDi locateNodes node contains duplicate authority-relevant fields", + ), + Self::MissingResultNodeType => formatter + .write_str("WebDriver BiDi locateNodes node is missing its remote-value type"), + Self::InvalidResultNodeType => formatter + .write_str("WebDriver BiDi locateNodes node type is not a JSON string"), + Self::InvalidResultNodeSharedId => formatter + .write_str("WebDriver BiDi locateNodes node sharedId is not a JSON string"), + Self::ResultAdmission(error) => write!( + formatter, + "WebDriver BiDi locateNodes wire result rejected node admission: {error}" + ), + Self::ResultParserInvariant => formatter.write_str( + "WebDriver BiDi locateNodes result parser invariant failed after envelope validation", + ), } } } @@ -37,6 +89,16 @@ impl Error for WebDriverBiDiLocateNodesResponseDocumentError { match self { Self::Parse(error) => Some(error), Self::Envelope(error) => Some(error), + Self::ResultAdmission(error) => Some(error), + Self::MissingResultNodes + | Self::InvalidResultNodes + | Self::DuplicateResultNodes + | Self::InvalidResultNode + | Self::DuplicateResultNodeField + | Self::MissingResultNodeType + | Self::InvalidResultNodeType + | Self::InvalidResultNodeSharedId + | Self::ResultParserInvariant => None, } } } @@ -63,4 +125,42 @@ impl WebDriverBiDiLocateNodesCommand { self.correlate_response_envelope(parsed.kind(), parsed.response_id()) .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope) } + + /// Consume one bounded raw `locateNodes` response through exact wire-derived node admission. + /// + /// The same bounded document first passes the complete response-envelope parser, exact command + /// correlation, and success-only conversion. Only then does the result parser derive the exact + /// `result.nodes` array from that already-validated wire document. Decoded duplicate `nodes`, + /// `type`, or `sharedId` fields fail closed, JSON-escaped protocol metadata is decoded before + /// admission, and the existing correlated-result boundary enforces the command's exact + /// `maxNodeCount` before normalizing node references. Callers cannot supply replacement node + /// metadata to this method. + /// + /// Success remains untrusted transport evidence. It does not authenticate Chromium, + /// ChromeDriver, WebSocket/TLS provenance, or an adapter process; prove current + /// session/context/origin/document authority; mint OriginWeave node handles; authorize policy + /// or typed input; execute browser I/O; or prove a post-condition. + pub fn admit_response_document_nodes( + self, + document: BoundedWebDriverBiDiResponseDocument, + ) -> Result + { + let parsed = document + .parse_command_response() + .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Parse)?; + let correlated = self + .correlate_response_envelope(parsed.kind(), parsed.response_id()) + .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope)?; + let validated = correlated + .into_validated_success() + .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope)?; + let wire_nodes = locate_nodes_result_document::parse_wire_locate_nodes_result(parsed.as_str())?; + let admission_parts = wire_nodes + .iter() + .map(locate_nodes_result_document::WireLocateNodesNode::as_admission_parts) + .collect::>(); + validated + .admit_result_nodes(&admission_parts) + .map_err(WebDriverBiDiLocateNodesResponseDocumentError::ResultAdmission) + } } diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs new file mode 100644 index 000000000..b5c46b3ba --- /dev/null +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs @@ -0,0 +1,455 @@ +use super::WebDriverBiDiLocateNodesResponseDocumentError; + +pub(super) struct WireLocateNodesNode { + remote_type: String, + shared_id: Option, +} + +impl WireLocateNodesNode { + pub(super) fn as_admission_parts(&self) -> (&str, Option<&str>) { + (self.remote_type.as_str(), self.shared_id.as_deref()) + } +} + +pub(super) fn parse_wire_locate_nodes_result( + input: &str, +) -> Result, WebDriverBiDiLocateNodesResponseDocumentError> { + ResultParser::new(input).parse() +} + +struct ResultParser<'input> { + input: &'input [u8], + position: usize, +} + +impl<'input> ResultParser<'input> { + const fn new(input: &'input str) -> Self { + Self { + input: input.as_bytes(), + position: 0, + } + } + + fn parse( + mut self, + ) -> Result, WebDriverBiDiLocateNodesResponseDocumentError> { + self.skip_whitespace(); + self.expect_byte(b'{')?; + self.skip_whitespace(); + + loop { + if self.peek_byte() == Some(b'}') { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant); + } + let field_name = self.parse_string()?; + self.skip_whitespace(); + self.expect_byte(b':')?; + self.skip_whitespace(); + if field_name == "result" { + return self.parse_result_object(); + } + self.skip_value()?; + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + } + Some(b'}') => { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ); + } + _ => { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ); + } + } + } + } + + fn parse_result_object( + &mut self, + ) -> Result, WebDriverBiDiLocateNodesResponseDocumentError> { + if self.peek_byte() != Some(b'{') { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodes); + } + self.position += 1; + self.skip_whitespace(); + let mut nodes = None; + + if self.peek_byte() == Some(b'}') { + self.position += 1; + return Err(WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodes); + } + + loop { + let field_name = self.parse_string()?; + self.skip_whitespace(); + self.expect_byte(b':')?; + self.skip_whitespace(); + if field_name == "nodes" { + if nodes.is_some() { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodes); + } + nodes = Some(self.parse_nodes_array()?); + } else { + self.skip_value()?; + } + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + } + Some(b'}') => { + self.position += 1; + break; + } + _ => { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ); + } + } + } + + nodes.ok_or(WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodes) + } + + fn parse_nodes_array( + &mut self, + ) -> Result, WebDriverBiDiLocateNodesResponseDocumentError> { + if self.peek_byte() != Some(b'[') { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodes); + } + self.position += 1; + self.skip_whitespace(); + let mut nodes = Vec::new(); + if self.peek_byte() == Some(b']') { + self.position += 1; + return Ok(nodes); + } + + loop { + nodes.push(self.parse_node()?); + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + } + Some(b']') => { + self.position += 1; + return Ok(nodes); + } + _ => { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ); + } + } + } + } + + fn parse_node( + &mut self, + ) -> Result { + if self.peek_byte() != Some(b'{') { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNode); + } + self.position += 1; + self.skip_whitespace(); + let mut remote_type = None; + let mut shared_id = None; + let mut shared_id_seen = false; + + if self.peek_byte() == Some(b'}') { + self.position += 1; + return Err(WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodeType); + } + + loop { + let field_name = self.parse_string()?; + self.skip_whitespace(); + self.expect_byte(b':')?; + self.skip_whitespace(); + match field_name.as_str() { + "type" => { + if remote_type.is_some() { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodeField, + ); + } + if self.peek_byte() != Some(b'"') { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodeType, + ); + } + remote_type = Some(self.parse_string()?); + } + "sharedId" => { + if shared_id_seen { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodeField, + ); + } + shared_id_seen = true; + if self.peek_byte() != Some(b'"') { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodeSharedId, + ); + } + shared_id = Some(self.parse_string()?); + } + _ => self.skip_value()?, + } + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + } + Some(b'}') => { + self.position += 1; + break; + } + _ => { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ); + } + } + } + + Ok(WireLocateNodesNode { + remote_type: remote_type + .ok_or(WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodeType)?, + shared_id, + }) + } + + fn skip_value(&mut self) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { + match self.peek_byte() { + Some(b'{') => self.skip_object(), + Some(b'[') => self.skip_array(), + Some(b'"') => { + let _value = self.parse_string()?; + Ok(()) + } + Some(b'-' | b'0'..=b'9') => self.skip_number(), + Some(b't') => self.skip_literal(b"true"), + Some(b'f') => self.skip_literal(b"false"), + Some(b'n') => self.skip_literal(b"null"), + _ => Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant), + } + } + + fn skip_object(&mut self) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { + self.expect_byte(b'{')?; + self.skip_whitespace(); + if self.peek_byte() == Some(b'}') { + self.position += 1; + return Ok(()); + } + loop { + let _field_name = self.parse_string()?; + self.skip_whitespace(); + self.expect_byte(b':')?; + self.skip_whitespace(); + self.skip_value()?; + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + } + Some(b'}') => { + self.position += 1; + return Ok(()); + } + _ => { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ); + } + } + } + } + + fn skip_array(&mut self) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { + self.expect_byte(b'[')?; + self.skip_whitespace(); + if self.peek_byte() == Some(b']') { + self.position += 1; + return Ok(()); + } + loop { + self.skip_value()?; + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + } + Some(b']') => { + self.position += 1; + return Ok(()); + } + _ => { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ); + } + } + } + } + + fn skip_number(&mut self) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { + let start = self.position; + while let Some(byte) = self.peek_byte() { + if matches!(byte, b',' | b']' | b'}' | b' ' | b'\t' | b'\r' | b'\n') { + break; + } + self.position += 1; + } + if self.position == start { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant); + } + Ok(()) + } + + fn skip_literal( + &mut self, + literal: &[u8], + ) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { + for expected in literal { + self.expect_byte(*expected)?; + } + Ok(()) + } + + fn parse_string(&mut self) -> Result { + self.expect_byte(b'"')?; + let mut decoded = Vec::new(); + loop { + let byte = self + .peek_byte() + .ok_or(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant)?; + match byte { + b'"' => { + self.position += 1; + return String::from_utf8(decoded).map_err(|_error| { + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant + }); + } + b'\\' => { + self.position += 1; + self.parse_escape(&mut decoded)?; + } + 0x00..=0x1f => { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ); + } + _ => { + decoded.push(byte); + self.position += 1; + } + } + } + } + + fn parse_escape( + &mut self, + decoded: &mut Vec, + ) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { + let escaped = self + .peek_byte() + .ok_or(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant)?; + self.position += 1; + match escaped { + b'"' => decoded.push(b'"'), + b'\\' => decoded.push(b'\\'), + b'/' => decoded.push(b'/'), + b'b' => decoded.push(0x08), + b'f' => decoded.push(0x0c), + b'n' => decoded.push(b'\n'), + b'r' => decoded.push(b'\r'), + b't' => decoded.push(b'\t'), + b'u' => self.parse_unicode_escape(decoded)?, + _ => return Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant), + } + Ok(()) + } + + fn parse_unicode_escape( + &mut self, + decoded: &mut Vec, + ) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { + let first = self.parse_hex_quad()?; + let scalar = if (0xd800..=0xdbff).contains(&first) { + self.expect_byte(b'\\')?; + self.expect_byte(b'u')?; + let second = self.parse_hex_quad()?; + if !(0xdc00..=0xdfff).contains(&second) { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant); + } + 0x1_0000 + (((u32::from(first) - 0xd800) << 10) | (u32::from(second) - 0xdc00)) + } else { + if (0xdc00..=0xdfff).contains(&first) { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant); + } + u32::from(first) + }; + let character = char::from_u32(scalar) + .ok_or(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant)?; + let mut encoded = [0_u8; 4]; + decoded.extend_from_slice(character.encode_utf8(&mut encoded).as_bytes()); + Ok(()) + } + + fn parse_hex_quad(&mut self) -> Result { + let mut value = 0_u16; + for _ in 0..4 { + let byte = self + .peek_byte() + .ok_or(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant)?; + self.position += 1; + let digit = match byte { + b'0'..=b'9' => u16::from(byte - b'0'), + b'a'..=b'f' => u16::from(byte - b'a' + 10), + b'A'..=b'F' => u16::from(byte - b'A' + 10), + _ => { + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ); + } + }; + value = (value << 4) | digit; + } + Ok(value) + } + + fn expect_byte( + &mut self, + expected: u8, + ) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { + if self.peek_byte() != Some(expected) { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant); + } + self.position += 1; + Ok(()) + } + + fn skip_whitespace(&mut self) { + while matches!(self.peek_byte(), Some(b' ' | b'\t' | b'\r' | b'\n')) { + self.position += 1; + } + } + + fn peek_byte(&self) -> Option { + self.input.get(self.position).copied() + } +} From 2f12879694638b8d503be7d0b392a48dff5d880b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:42:42 -0700 Subject: [PATCH 030/118] style(core): apply canonical response document formatting --- .../src/webdriver_bidi_response_document_correlation.rs | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 570ec9ddc..71092b616 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -143,8 +143,10 @@ impl WebDriverBiDiLocateNodesCommand { pub fn admit_response_document_nodes( self, document: BoundedWebDriverBiDiResponseDocument, - ) -> Result - { + ) -> Result< + ValidatedWebDriverBiDiLocateNodesResult, + WebDriverBiDiLocateNodesResponseDocumentError, + > { let parsed = document .parse_command_response() .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Parse)?; @@ -154,7 +156,8 @@ impl WebDriverBiDiLocateNodesCommand { let validated = correlated .into_validated_success() .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope)?; - let wire_nodes = locate_nodes_result_document::parse_wire_locate_nodes_result(parsed.as_str())?; + let wire_nodes = + locate_nodes_result_document::parse_wire_locate_nodes_result(parsed.as_str())?; let admission_parts = wire_nodes .iter() .map(locate_nodes_result_document::WireLocateNodesNode::as_admission_parts) From c8d6d9a39601cb4ad6aaa0e0e731b9c328cb1f9b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 01:43:25 -0700 Subject: [PATCH 031/118] style(core): apply canonical wire node parser formatting --- .../locate_nodes_result_document.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs index b5c46b3ba..5e61b408c 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs @@ -91,7 +91,9 @@ impl<'input> ResultParser<'input> { self.skip_whitespace(); if field_name == "nodes" { if nodes.is_some() { - return Err(WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodes); + return Err( + WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodes, + ); } nodes = Some(self.parse_nodes_array()?); } else { From e7b81bc6d5a20d7b6503c9cd397984197b765780 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 03:13:01 -0700 Subject: [PATCH 032/118] test(core): cover locateNodes document failure boundaries --- ...webdriver_bidi_locate_nodes_wire_result.rs | 64 ++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs index 349d7912f..9262ea8a2 100644 --- a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs @@ -2,7 +2,7 @@ use std::error::Error; use originweave_core::{ BoundedWebDriverBiDiResponseDocument, WebDriverBiDiAccessibilityQuery, - WebDriverBiDiLocateNodesCommand, + WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesResponseDocumentError, }; fn locate_nodes_command( @@ -123,3 +123,65 @@ fn wire_result_decodes_json_escaped_protocol_fields_before_admission() -> Result assert_eq!(admitted.nodes()[0].shared_id(), "node-α"); Ok(()) } + +#[test] +fn wire_result_boundary_preserves_parse_correlation_and_success_only_failures() +-> Result<(), Box> { + let malformed = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{},}"#, + )?; + assert!(matches!( + locate_nodes_command(42, 1)?.admit_response_document_nodes(malformed), + Err(WebDriverBiDiLocateNodesResponseDocumentError::Parse(_)) + )); + + let mismatched = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":41,"result":{"nodes":[]}}"#, + )?; + assert!(matches!( + locate_nodes_command(42, 1)?.admit_response_document_nodes(mismatched), + Err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope(_)) + )); + + let error_response = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"error","id":42,"error":"invalid argument","message":"bad request"}"#, + )?; + assert!(matches!( + locate_nodes_command(42, 1)?.admit_response_document_nodes(error_response), + Err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope(_)) + )); + Ok(()) +} + +#[test] +fn wire_result_document_error_display_and_sources_cover_result_failure_variants() +-> Result<(), Box> { + let source_free = [ + WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodes, + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodes, + WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodes, + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNode, + WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodeField, + WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodeType, + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodeType, + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodeSharedId, + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant, + ]; + for error in source_free { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_none()); + } + + let over_budget = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node","sharedId":"node-a"},{"type":"node","sharedId":"node-b"}]}}"#, + )?; + let result = locate_nodes_command(42, 1)?.admit_response_document_nodes(over_budget); + match result { + Err(error @ WebDriverBiDiLocateNodesResponseDocumentError::ResultAdmission(_)) => { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_some()); + } + _ => panic!("over-budget wire result must preserve result-admission error evidence"), + } + Ok(()) +} From 16591aba4627520984a8f1125f402f4137469e5f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 03:13:49 -0700 Subject: [PATCH 033/118] test(core): keep failure-evidence regressions clippy-clean --- .../webdriver_bidi_locate_nodes_wire_result.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs index 9262ea8a2..db9504f6c 100644 --- a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs @@ -176,12 +176,15 @@ fn wire_result_document_error_display_and_sources_cover_result_failure_variants( r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node","sharedId":"node-a"},{"type":"node","sharedId":"node-b"}]}}"#, )?; let result = locate_nodes_command(42, 1)?.admit_response_document_nodes(over_budget); - match result { - Err(error @ WebDriverBiDiLocateNodesResponseDocumentError::ResultAdmission(_)) => { - assert!(!error.to_string().is_empty()); - assert!(error.source().is_some()); + let error = match result { + Err(error @ WebDriverBiDiLocateNodesResponseDocumentError::ResultAdmission(_)) => error, + _ => { + return Err( + "over-budget wire result must preserve result-admission error evidence".into(), + ); } - _ => panic!("over-budget wire result must preserve result-admission error evidence"), - } + }; + assert!(!error.to_string().is_empty()); + assert!(error.source().is_some()); Ok(()) } From 858bcf4f1f9f1071c3d19c762d8895d38082fd6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 03:15:14 -0700 Subject: [PATCH 034/118] test(core): cover locateNodes second-pass parser invariants --- .../locate_nodes_result_document.rs | 179 ++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs index 5e61b408c..d03542926 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs @@ -455,3 +455,182 @@ impl<'input> ResultParser<'input> { self.input.get(self.position).copied() } } + +#[cfg(test)] +mod tests { + use super::*; + + const INVARIANT: WebDriverBiDiLocateNodesResponseDocumentError = + WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant; + + #[test] + fn second_pass_parser_covers_valid_skipped_value_and_escape_shapes() { + let raw = concat!( + " \n{\t\"metadata\": [true,false,null,{\"a\":1,\"b\":2}],", + "\"result\":{", + "\"emptyObject\":{},\"emptyArray\":[],", + "\"object\":{\"first\":1,\"second\":2},", + "\"array\":[true,false,null],", + "\"escaped\":\"\\\"\\\\\\/\\b\\f\\n\\r\\t\",", + "\"number\":-1.25e+2,\"truth\":true,\"falsehood\":false,\"nothing\":null,", + "\"nodes\":[{", + "\"ignored\":{\"nested\":[1,2]},", + "\"type\":\"no\\u0064e\",", + "\"sharedId\":\"node-\\u0041-\\u00E9-\\u263A-\\uD83D\\uDE00-\\u00af-\\u00AF\"", + "}]}}" + ); + let nodes = parse_wire_locate_nodes_result(raw) + .into_iter() + .flatten() + .collect::>(); + + assert_eq!(nodes.len(), 1); + assert_eq!(nodes[0].remote_type, "node"); + assert_eq!(nodes[0].shared_id.as_deref(), Some("node-A-é-☺-😀-¯-¯")); + } + + #[test] + fn second_pass_parser_rejects_structural_and_typed_result_faults() { + let cases = [ + ("", INVARIANT), + ("{}", INVARIANT), + (r#"{"metadata":0}"#, INVARIANT), + (r#"{"metadata":0]"#, INVARIANT), + (r#"{"metadata" 0,"result":{"nodes":[]}}"#, INVARIANT), + (r#"{metadata:0,"result":{"nodes":[]}}"#, INVARIANT), + ( + r#"{"result":[]}"#, + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodes, + ), + ( + r#"{"result":{}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodes, + ), + ( + r#"{"result":{"other":0}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodes, + ), + ( + r#"{"result":{"nodes":0}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodes, + ), + ( + r#"{"result":{"nodes":[0]}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNode, + ), + ( + r#"{"result":{"nodes":[{}]}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodeType, + ), + ( + r#"{"result":{"nodes":[{"sharedId":"node-a"}]}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodeType, + ), + ( + r#"{"result":{"nodes":[{"type":0}]}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodeType, + ), + ( + r#"{"result":{"nodes":[{"type":"node","sharedId":0}]}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodeSharedId, + ), + ( + r#"{"result":{"nodes":[],"nodes":[]}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodes, + ), + ( + r#"{"result":{"nodes":[{"type":"node","type":"node"}]}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodeField, + ), + ( + r#"{"result":{"nodes":[{"type":"node","sharedId":"a","sharedId":"b"}]}}"#, + WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodeField, + ), + (r#"{"result":{"nodes":[] "other":0}}"#, INVARIANT), + (r#"{"result":{"nodes":[{"type":"node"} 0]}}"#, INVARIANT), + ( + r#"{"result":{"nodes":[{"type":"node" "sharedId":"node-a"}]}}"#, + INVARIANT, + ), + ]; + + for (raw, expected) in cases { + assert_eq!(parse_wire_locate_nodes_result(raw), Err(expected)); + } + } + + #[test] + fn skip_helpers_cover_empty_nonempty_and_malformed_containers() { + let mut empty_object = ResultParser::new("{}"); + assert_eq!(empty_object.skip_object(), Ok(())); + + let mut object = ResultParser::new(r#"{"a":0,"b":1}"#); + assert_eq!(object.skip_object(), Ok(())); + + let mut malformed_object = ResultParser::new(r#"{"a":0 "b":1}"#); + assert_eq!(malformed_object.skip_object(), Err(INVARIANT)); + + let mut empty_array = ResultParser::new("[]"); + assert_eq!(empty_array.skip_array(), Ok(())); + + let mut array = ResultParser::new("[0,1]"); + assert_eq!(array.skip_array(), Ok(())); + + let mut malformed_array = ResultParser::new("[0 1]"); + assert_eq!(malformed_array.skip_array(), Err(INVARIANT)); + + let mut unknown = ResultParser::new("?"); + assert_eq!(unknown.skip_value(), Err(INVARIANT)); + + let mut empty_number = ResultParser::new(""); + assert_eq!(empty_number.skip_number(), Err(INVARIANT)); + + let mut terminal_number = ResultParser::new("123"); + assert_eq!(terminal_number.skip_number(), Ok(())); + assert_eq!(terminal_number.peek_byte(), None); + + let mut truncated_literal = ResultParser::new("tru"); + assert_eq!(truncated_literal.skip_literal(b"true"), Err(INVARIANT)); + } + + #[test] + fn string_decoder_rejects_all_second_pass_escape_invariants() { + let mut missing_open_quote = ResultParser::new("plain"); + assert_eq!(missing_open_quote.parse_string(), Err(INVARIANT)); + + let mut unterminated = ResultParser::new("\"plain"); + assert_eq!(unterminated.parse_string(), Err(INVARIANT)); + + let control_bytes = [b'\"', 0x01, b'\"']; + let mut control = ResultParser { + input: &control_bytes, + position: 0, + }; + assert_eq!(control.parse_string(), Err(INVARIANT)); + + let invalid_utf8_bytes = [b'\"', 0xff, b'\"']; + let mut invalid_utf8 = ResultParser { + input: &invalid_utf8_bytes, + position: 0, + }; + assert_eq!(invalid_utf8.parse_string(), Err(INVARIANT)); + + let mut missing_escape = ResultParser::new("\"\\"); + assert_eq!(missing_escape.parse_string(), Err(INVARIANT)); + + let mut invalid_escape = ResultParser::new(r#""\x""#); + assert_eq!(invalid_escape.parse_string(), Err(INVARIANT)); + + for raw in [ + r#""\uD83D""#, + r#""\uD83D\x0000""#, + r#""\uD83D\u0041""#, + r#""\uDE00""#, + r#""\u12""#, + r#""\u00G0""#, + ] { + let mut parser = ResultParser::new(raw); + assert_eq!(parser.parse_string(), Err(INVARIANT)); + } + } +} From da7ffe9334f2eb957bc2b2fb3b53e72eddd3837e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:09:07 -0700 Subject: [PATCH 035/118] test(core): avoid node debug equality requirement --- .../locate_nodes_result_document.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs index d03542926..622ab15b8 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs @@ -555,7 +555,10 @@ mod tests { ]; for (raw, expected) in cases { - assert_eq!(parse_wire_locate_nodes_result(raw), Err(expected)); + assert!(matches!( + parse_wire_locate_nodes_result(raw), + Err(error) if error == expected + )); } } From 1b0f6ada0f789d9cd7cba6eebc154f0ed5272195 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:09:41 -0700 Subject: [PATCH 036/118] style(core): apply canonical wire-result formatting --- .../tests/webdriver_bidi_locate_nodes_wire_result.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs index db9504f6c..fcc1ba946 100644 --- a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs @@ -127,9 +127,8 @@ fn wire_result_decodes_json_escaped_protocol_fields_before_admission() -> Result #[test] fn wire_result_boundary_preserves_parse_correlation_and_success_only_failures() -> Result<(), Box> { - let malformed = BoundedWebDriverBiDiResponseDocument::new( - r#"{"type":"success","id":42,"result":{},}"#, - )?; + let malformed = + BoundedWebDriverBiDiResponseDocument::new(r#"{"type":"success","id":42,"result":{},}"#)?; assert!(matches!( locate_nodes_command(42, 1)?.admit_response_document_nodes(malformed), Err(WebDriverBiDiLocateNodesResponseDocumentError::Parse(_)) From cbbc4d6661d0f78ef85d2dc991232671a1c0a0f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:19:40 -0700 Subject: [PATCH 037/118] test(core): cover second-pass parser failure propagation --- .../locate_nodes_result_document.rs | 41 +++++++++++++++++-- 1 file changed, 38 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs index 622ab15b8..3ee6af71b 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs @@ -400,9 +400,6 @@ impl<'input> ResultParser<'input> { } 0x1_0000 + (((u32::from(first) - 0xd800) << 10) | (u32::from(second) - 0xdc00)) } else { - if (0xdc00..=0xdfff).contains(&first) { - return Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant); - } u32::from(first) }; let character = char::from_u32(scalar) @@ -552,6 +549,21 @@ mod tests { r#"{"result":{"nodes":[{"type":"node" "sharedId":"node-a"}]}}"#, INVARIANT, ), + (r#"{"metadata":?,"result":{"nodes":[]}}"#, INVARIANT), + (r#"{"result":{?}}"#, INVARIANT), + (r#"{"result":{"nodes" []}}"#, INVARIANT), + (r#"{"result":{"other":?,"nodes":[]}}"#, INVARIANT), + (r#"{"result":{"nodes":[{?}]}}"#, INVARIANT), + (r#"{"result":{"nodes":[{"type" "node"}]}}"#, INVARIANT), + (r#"{"result":{"nodes":[{"type":"\x"}]}}"#, INVARIANT), + ( + r#"{"result":{"nodes":[{"type":"node","sharedId":"\x"}]}}"#, + INVARIANT, + ), + ( + r#"{"result":{"nodes":[{"ignored":?,"type":"node"}]}}"#, + INVARIANT, + ), ]; for (raw, expected) in cases { @@ -592,6 +604,27 @@ mod tests { assert_eq!(terminal_number.skip_number(), Ok(())); assert_eq!(terminal_number.peek_byte(), None); + let mut malformed_string_value = ResultParser::new(r#""\x""#); + assert_eq!(malformed_string_value.skip_value(), Err(INVARIANT)); + + let mut wrong_object_opener = ResultParser::new("[]"); + assert_eq!(wrong_object_opener.skip_object(), Err(INVARIANT)); + + let mut malformed_object_key = ResultParser::new("{?}"); + assert_eq!(malformed_object_key.skip_object(), Err(INVARIANT)); + + let mut missing_object_colon = ResultParser::new(r#"{"a" 0}"#); + assert_eq!(missing_object_colon.skip_object(), Err(INVARIANT)); + + let mut malformed_object_value = ResultParser::new(r#"{"a":?}"#); + assert_eq!(malformed_object_value.skip_object(), Err(INVARIANT)); + + let mut wrong_array_opener = ResultParser::new("{}"); + assert_eq!(wrong_array_opener.skip_array(), Err(INVARIANT)); + + let mut malformed_array_value = ResultParser::new("[?]"); + assert_eq!(malformed_array_value.skip_array(), Err(INVARIANT)); + let mut truncated_literal = ResultParser::new("tru"); assert_eq!(truncated_literal.skip_literal(b"true"), Err(INVARIANT)); } @@ -628,8 +661,10 @@ mod tests { r#""\uD83D""#, r#""\uD83D\x0000""#, r#""\uD83D\u0041""#, + "\"\\uD83D\\u12", r#""\uDE00""#, r#""\u12""#, + "\"\\u12", r#""\u00G0""#, ] { let mut parser = ResultParser::new(raw); From 755ba0682c0242005a1d668a700a975195160310 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 06:17:16 -0700 Subject: [PATCH 038/118] fix(core): keep second-pass string decoding utf8-safe --- .../locate_nodes_result_document.rs | 62 +++++++------------ 1 file changed, 23 insertions(+), 39 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs index 3ee6af71b..242b2a081 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs @@ -18,16 +18,13 @@ pub(super) fn parse_wire_locate_nodes_result( } struct ResultParser<'input> { - input: &'input [u8], + input: &'input str, position: usize, } impl<'input> ResultParser<'input> { const fn new(input: &'input str) -> Self { - Self { - input: input.as_bytes(), - position: 0, - } + Self { input, position: 0 } } fn parse( @@ -334,21 +331,23 @@ impl<'input> ResultParser<'input> { fn parse_string(&mut self) -> Result { self.expect_byte(b'"')?; - let mut decoded = Vec::new(); + let mut decoded = String::new(); + let mut literal_start = self.position; loop { let byte = self .peek_byte() .ok_or(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant)?; match byte { b'"' => { + decoded.push_str(&self.input[literal_start..self.position]); self.position += 1; - return String::from_utf8(decoded).map_err(|_error| { - WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant - }); + return Ok(decoded); } b'\\' => { + decoded.push_str(&self.input[literal_start..self.position]); self.position += 1; self.parse_escape(&mut decoded)?; + literal_start = self.position; } 0x00..=0x1f => { return Err( @@ -356,7 +355,6 @@ impl<'input> ResultParser<'input> { ); } _ => { - decoded.push(byte); self.position += 1; } } @@ -365,21 +363,21 @@ impl<'input> ResultParser<'input> { fn parse_escape( &mut self, - decoded: &mut Vec, + decoded: &mut String, ) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { let escaped = self .peek_byte() .ok_or(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant)?; self.position += 1; match escaped { - b'"' => decoded.push(b'"'), - b'\\' => decoded.push(b'\\'), - b'/' => decoded.push(b'/'), - b'b' => decoded.push(0x08), - b'f' => decoded.push(0x0c), - b'n' => decoded.push(b'\n'), - b'r' => decoded.push(b'\r'), - b't' => decoded.push(b'\t'), + b'"' => decoded.push('"'), + b'\\' => decoded.push('\\'), + b'/' => decoded.push('/'), + b'b' => decoded.push('\u{0008}'), + b'f' => decoded.push('\u{000c}'), + b'n' => decoded.push('\n'), + b'r' => decoded.push('\r'), + b't' => decoded.push('\t'), b'u' => self.parse_unicode_escape(decoded)?, _ => return Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant), } @@ -388,7 +386,7 @@ impl<'input> ResultParser<'input> { fn parse_unicode_escape( &mut self, - decoded: &mut Vec, + decoded: &mut String, ) -> Result<(), WebDriverBiDiLocateNodesResponseDocumentError> { let first = self.parse_hex_quad()?; let scalar = if (0xd800..=0xdbff).contains(&first) { @@ -404,8 +402,7 @@ impl<'input> ResultParser<'input> { }; let character = char::from_u32(scalar) .ok_or(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant)?; - let mut encoded = [0_u8; 4]; - decoded.extend_from_slice(character.encode_utf8(&mut encoded).as_bytes()); + decoded.push(character); Ok(()) } @@ -449,7 +446,7 @@ impl<'input> ResultParser<'input> { } fn peek_byte(&self) -> Option { - self.input.get(self.position).copied() + self.input.as_bytes().get(self.position).copied() } } @@ -469,6 +466,7 @@ mod tests { "\"object\":{\"first\":1,\"second\":2},", "\"array\":[true,false,null],", "\"escaped\":\"\\\"\\\\\\/\\b\\f\\n\\r\\t\",", + "\"utf8\":\"é\",", "\"number\":-1.25e+2,\"truth\":true,\"falsehood\":false,\"nothing\":null,", "\"nodes\":[{", "\"ignored\":{\"nested\":[1,2]},", @@ -567,10 +565,7 @@ mod tests { ]; for (raw, expected) in cases { - assert!(matches!( - parse_wire_locate_nodes_result(raw), - Err(error) if error == expected - )); + assert_eq!(parse_wire_locate_nodes_result(raw).err(), Some(expected)); } } @@ -637,20 +632,9 @@ mod tests { let mut unterminated = ResultParser::new("\"plain"); assert_eq!(unterminated.parse_string(), Err(INVARIANT)); - let control_bytes = [b'\"', 0x01, b'\"']; - let mut control = ResultParser { - input: &control_bytes, - position: 0, - }; + let mut control = ResultParser::new("\"\u{0001}\""); assert_eq!(control.parse_string(), Err(INVARIANT)); - let invalid_utf8_bytes = [b'\"', 0xff, b'\"']; - let mut invalid_utf8 = ResultParser { - input: &invalid_utf8_bytes, - position: 0, - }; - assert_eq!(invalid_utf8.parse_string(), Err(INVARIANT)); - let mut missing_escape = ResultParser::new("\"\\"); assert_eq!(missing_escape.parse_string(), Err(INVARIANT)); From 70d85f97a30d5f661fe47fbf19bc4589be11c39a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:12:24 -0700 Subject: [PATCH 039/118] test(core): cover wire node admission tuple --- ...iver_bidi_response_document_correlation.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 71092b616..c121cb434 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -167,3 +167,26 @@ impl WebDriverBiDiLocateNodesCommand { .map_err(WebDriverBiDiLocateNodesResponseDocumentError::ResultAdmission) } } + +#[cfg(test)] +mod tests { + use super::locate_nodes_result_document::{ + parse_wire_locate_nodes_result, WireLocateNodesNode, + }; + + #[test] + fn wire_node_admission_parts_preserve_wire_type_and_shared_id() { + let parsed = parse_wire_locate_nodes_result( + r#"{"result":{"nodes":[{"type":"node","sharedId":"node-17"}]}}"#, + ); + assert!(parsed.is_ok()); + + if let Ok(nodes) = parsed { + let admission_parts = nodes + .iter() + .map(WireLocateNodesNode::as_admission_parts) + .collect::>(); + assert_eq!(admission_parts, vec![("node", Some("node-17"))]); + } + } +} From 4089a727abe4c74057cf5dd1b6ccb55a0d7c7303 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 07:20:41 -0700 Subject: [PATCH 040/118] test(core): cover skipped wire result shapes --- ...iver_bidi_response_document_correlation.rs | 23 ------------------- ...webdriver_bidi_locate_nodes_wire_result.rs | 2 +- 2 files changed, 1 insertion(+), 24 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index c121cb434..71092b616 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -167,26 +167,3 @@ impl WebDriverBiDiLocateNodesCommand { .map_err(WebDriverBiDiLocateNodesResponseDocumentError::ResultAdmission) } } - -#[cfg(test)] -mod tests { - use super::locate_nodes_result_document::{ - parse_wire_locate_nodes_result, WireLocateNodesNode, - }; - - #[test] - fn wire_node_admission_parts_preserve_wire_type_and_shared_id() { - let parsed = parse_wire_locate_nodes_result( - r#"{"result":{"nodes":[{"type":"node","sharedId":"node-17"}]}}"#, - ); - assert!(parsed.is_ok()); - - if let Ok(nodes) = parsed { - let admission_parts = nodes - .iter() - .map(WireLocateNodesNode::as_admission_parts) - .collect::>(); - assert_eq!(admission_parts, vec![("node", Some("node-17"))]); - } - } -} diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs index fcc1ba946..4648e1a8d 100644 --- a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs @@ -22,7 +22,7 @@ fn locate_nodes_command( fn bounded_locate_nodes_document_admits_exact_wire_nodes_without_caller_selected_payload() -> Result<(), Box> { let document = BoundedWebDriverBiDiResponseDocument::new( - r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node","sharedId":"node-a"},{"type":"node","sharedId":"node-b","value":{"nodeType":1}}]}}"#, + r#"{"type":"success","id":42,"result":{"ignored":[true,false,null],"nodes":[{"type":"node","sharedId":"node-a"},{"type":"node","sharedId":"node-b","value":{"nodeType":1}}]}}"#, )?; let admitted = locate_nodes_command(42, 2)?.admit_response_document_nodes(document)?; From 14a4a39b87bb655bda2398a03cca436a7ed213a3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 09:12:31 -0700 Subject: [PATCH 041/118] test(core): cover wire node admission metadata --- ...iver_bidi_response_document_correlation.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 71092b616..339ad9aff 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -167,3 +167,25 @@ impl WebDriverBiDiLocateNodesCommand { .map_err(WebDriverBiDiLocateNodesResponseDocumentError::ResultAdmission) } } + +#[cfg(test)] +mod tests { + use super::locate_nodes_result_document::parse_wire_locate_nodes_result; + + #[test] + fn wire_node_admission_parts_preserve_wire_derived_metadata() { + let nodes = parse_wire_locate_nodes_result(concat!( + "{\"result\":{\"nodes\":[", + "{\"type\":\"node\",\"sharedId\":\"shared-1\"},", + "{\"type\":\"window\"}", + "]}}" + )) + .into_iter() + .flatten() + .collect::>(); + + assert_eq!(nodes.len(), 2); + assert_eq!(nodes[0].as_admission_parts(), ("node", Some("shared-1"))); + assert_eq!(nodes[1].as_admission_parts(), ("window", None)); + } +} From a2fe264cf627536e5502ad8a31f45080b471785a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:09:02 -0700 Subject: [PATCH 042/118] test(core): prioritize locateNodes result budget --- .../webdriver_bidi_locate_nodes_wire_result.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs index 4648e1a8d..ce5d7bcd3 100644 --- a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs @@ -50,6 +50,19 @@ fn wire_result_preserves_exact_command_node_budget() -> Result<(), Box Result<(), Box> { + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node","sharedId":"node-a"},{"type":1,"sharedId":"malformed-overflow-item"}]}}"#, + )?; + + assert!(matches!( + locate_nodes_command(42, 1)?.admit_response_document_nodes(document), + Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultAdmission(_)) + )); + Ok(()) +} + #[test] fn wire_result_rejects_missing_shared_id() -> Result<(), Box> { let document = BoundedWebDriverBiDiResponseDocument::new( From 3083e4eb8490c23e5f0c31973b89c26b11712c8c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:14:38 -0700 Subject: [PATCH 043/118] fix(core): enforce locateNodes budget before node decoding --- .../locate_nodes_result_document.rs | 60 ++++++++++++++++++- 1 file changed, 58 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs index 242b2a081..409d0f6f8 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation/locate_nodes_result_document.rs @@ -9,22 +9,51 @@ impl WireLocateNodesNode { pub(super) fn as_admission_parts(&self) -> (&str, Option<&str>) { (self.remote_type.as_str(), self.shared_id.as_deref()) } + + fn overflow_count_marker() -> Self { + Self { + remote_type: String::new(), + shared_id: None, + } + } } +#[cfg(test)] pub(super) fn parse_wire_locate_nodes_result( input: &str, ) -> Result, WebDriverBiDiLocateNodesResponseDocumentError> { ResultParser::new(input).parse() } +pub(super) fn parse_wire_locate_nodes_result_bounded( + input: &str, + max_node_count: u16, +) -> Result, WebDriverBiDiLocateNodesResponseDocumentError> { + ResultParser::with_node_budget(input, usize::from(max_node_count)).parse() +} + struct ResultParser<'input> { input: &'input str, position: usize, + max_node_count: Option, } impl<'input> ResultParser<'input> { + #[cfg(test)] const fn new(input: &'input str) -> Self { - Self { input, position: 0 } + Self { + input, + position: 0, + max_node_count: None, + } + } + + const fn with_node_budget(input: &'input str, max_node_count: usize) -> Self { + Self { + input, + position: 0, + max_node_count: Some(max_node_count), + } } fn parse( @@ -126,13 +155,25 @@ impl<'input> ResultParser<'input> { self.position += 1; self.skip_whitespace(); let mut nodes = Vec::new(); + let mut over_budget = false; if self.peek_byte() == Some(b']') { self.position += 1; return Ok(nodes); } loop { - nodes.push(self.parse_node()?); + let at_node_budget = self + .max_node_count + .is_some_and(|max_node_count| nodes.len() >= max_node_count); + if over_budget || at_node_budget { + self.skip_value()?; + if !over_budget { + nodes.push(WireLocateNodesNode::overflow_count_marker()); + over_budget = true; + } + } else { + nodes.push(self.parse_node()?); + } self.skip_whitespace(); match self.peek_byte() { Some(b',') => { @@ -569,6 +610,21 @@ mod tests { } } + #[test] + fn bounded_parser_uses_one_count_marker_and_skips_overflow_node_shapes() { + let nodes = parse_wire_locate_nodes_result_bounded( + r#"{"result":{"nodes":[{"type":"node","sharedId":"node-a"},{"type":1},{"type":2}]}}"#, + 1, + ) + .into_iter() + .flatten() + .collect::>(); + + assert_eq!(nodes.len(), 2); + assert_eq!(nodes[0].remote_type, "node"); + assert_eq!(nodes[1].as_admission_parts(), ("", None)); + } + #[test] fn skip_helpers_cover_empty_nonempty_and_malformed_containers() { let mut empty_object = ResultParser::new("{}"); From 6c3b0bdf427601c82ee864e43e96b2b105c0d718 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:15:21 -0700 Subject: [PATCH 044/118] fix(core): bind wire parsing to exact result budget --- ...iver_bidi_response_document_correlation.rs | 27 ++++++++++--------- 1 file changed, 15 insertions(+), 12 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 339ad9aff..703049543 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -27,15 +27,15 @@ pub enum WebDriverBiDiLocateNodesResponseDocumentError { InvalidResultNodes, /// The correlated success result repeated the decoded `nodes` field. DuplicateResultNodes, - /// One `nodes` array item was not a JSON object. + /// One in-budget `nodes` array item was not a JSON object. InvalidResultNode, - /// One node object repeated decoded `type` or `sharedId` authority-relevant metadata. + /// One in-budget node object repeated decoded `type` or `sharedId` authority-relevant metadata. DuplicateResultNodeField, - /// One node object omitted its required WebDriver BiDi remote-value `type` field. + /// One in-budget node object omitted its required WebDriver BiDi remote-value `type` field. MissingResultNodeType, - /// One node object's `type` field was not a JSON string. + /// One in-budget node object's `type` field was not a JSON string. InvalidResultNodeType, - /// One present node `sharedId` field was not a JSON string. + /// One present in-budget node `sharedId` field was not a JSON string. InvalidResultNodeSharedId, /// Exact command-budget or remote-node admission rejected the wire-derived node batch. ResultAdmission(WebDriverBiDiLocateNodesResultAdmissionError), @@ -130,11 +130,12 @@ impl WebDriverBiDiLocateNodesCommand { /// /// The same bounded document first passes the complete response-envelope parser, exact command /// correlation, and success-only conversion. Only then does the result parser derive the exact - /// `result.nodes` array from that already-validated wire document. Decoded duplicate `nodes`, - /// `type`, or `sharedId` fields fail closed, JSON-escaped protocol metadata is decoded before - /// admission, and the existing correlated-result boundary enforces the command's exact - /// `maxNodeCount` before normalizing node references. Callers cannot supply replacement node - /// metadata to this method. + /// `result.nodes` array from that already-validated wire document. The command's exact + /// `maxNodeCount` is carried into this parser so overflow items are consumed only as generic JSON + /// and produce the existing result-budget failure before authority-relevant node metadata is + /// decoded or normalized. Decoded duplicate `nodes`, and duplicate or malformed in-budget + /// `type`/`sharedId` fields, fail closed. JSON-escaped protocol metadata is decoded before + /// admission, and callers cannot supply replacement node metadata to this method. /// /// Success remains untrusted transport evidence. It does not authenticate Chromium, /// ChromeDriver, WebSocket/TLS provenance, or an adapter process; prove current @@ -156,8 +157,10 @@ impl WebDriverBiDiLocateNodesCommand { let validated = correlated .into_validated_success() .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope)?; - let wire_nodes = - locate_nodes_result_document::parse_wire_locate_nodes_result(parsed.as_str())?; + let wire_nodes = locate_nodes_result_document::parse_wire_locate_nodes_result_bounded( + parsed.as_str(), + validated.max_node_count(), + )?; let admission_parts = wire_nodes .iter() .map(locate_nodes_result_document::WireLocateNodesNode::as_admission_parts) From 850dbf16cfe02021d8460b9f8dbb5a1d3f0e7e41 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 10:26:15 -0700 Subject: [PATCH 045/118] test(core): cover bounded overflow parser invariant --- ...iver_bidi_response_document_correlation.rs | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 703049543..7ee7c78ac 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -173,7 +173,10 @@ impl WebDriverBiDiLocateNodesCommand { #[cfg(test)] mod tests { - use super::locate_nodes_result_document::parse_wire_locate_nodes_result; + use super::locate_nodes_result_document::{ + parse_wire_locate_nodes_result, parse_wire_locate_nodes_result_bounded, + }; + use super::WebDriverBiDiLocateNodesResponseDocumentError; #[test] fn wire_node_admission_parts_preserve_wire_derived_metadata() { @@ -191,4 +194,21 @@ mod tests { assert_eq!(nodes[0].as_admission_parts(), ("node", Some("shared-1"))); assert_eq!(nodes[1].as_admission_parts(), ("window", None)); } + + #[test] + fn bounded_overflow_parser_preserves_invalid_generic_json_invariant() { + let result = parse_wire_locate_nodes_result_bounded( + concat!( + "{\"result\":{\"nodes\":[", + "{\"type\":\"node\",\"sharedId\":\"shared-1\"},", + "?]}}" + ), + 1, + ); + + assert!(matches!( + result, + Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant) + )); + } } From e373a73b22e24ebbf4755d2b7c63989fda18de01 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 11:09:17 -0700 Subject: [PATCH 046/118] test(core): close bounded overflow coverage invariant --- .../webdriver_bidi_response_document_correlation.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 7ee7c78ac..84635fa18 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -173,10 +173,10 @@ impl WebDriverBiDiLocateNodesCommand { #[cfg(test)] mod tests { + use super::WebDriverBiDiLocateNodesResponseDocumentError; use super::locate_nodes_result_document::{ parse_wire_locate_nodes_result, parse_wire_locate_nodes_result_bounded, }; - use super::WebDriverBiDiLocateNodesResponseDocumentError; #[test] fn wire_node_admission_parts_preserve_wire_derived_metadata() { @@ -206,9 +206,9 @@ mod tests { 1, ); - assert!(matches!( - result, - Err(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant) - )); + assert_eq!( + result.err(), + Some(WebDriverBiDiLocateNodesResponseDocumentError::ResultParserInvariant) + ); } } From dfc522f2ac3c2cc71108b6ea0fb7948b75c6cf12 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 11:31:29 -0700 Subject: [PATCH 047/118] test(core): require bounded BiDi byte admission --- ...webdriver_bidi_response_document_budget.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/crates/originweave-core/tests/webdriver_bidi_response_document_budget.rs b/crates/originweave-core/tests/webdriver_bidi_response_document_budget.rs index 7909c4f28..8d440272e 100644 --- a/crates/originweave-core/tests/webdriver_bidi_response_document_budget.rs +++ b/crates/originweave-core/tests/webdriver_bidi_response_document_budget.rs @@ -14,6 +14,28 @@ fn bounded_response_document_retains_exact_wire_text() -> Result<(), Box Result<(), Box> { + let raw = b" \r\n{\"id\":42,\"type\":\"success\",\"result\":{}}\t"; + let document = BoundedWebDriverBiDiResponseDocument::from_utf8_bytes(raw)?; + + assert_eq!(document.as_str(), std::str::from_utf8(raw)?); + + let invalid_utf8 = [b'{', 0xff, b'}']; + assert_eq!( + BoundedWebDriverBiDiResponseDocument::from_utf8_bytes(&invalid_utf8), + Err(WebDriverBiDiResponseDocumentAdmissionError::InvalidUtf8) + ); + + let oversized_invalid_utf8 = vec![0xff; MAX_WEBDRIVER_BIDI_RESPONSE_DOCUMENT_BYTES + 1]; + assert_eq!( + BoundedWebDriverBiDiResponseDocument::from_utf8_bytes(&oversized_invalid_utf8), + Err(WebDriverBiDiResponseDocumentAdmissionError::DocumentTooLarge) + ); + Ok(()) +} + #[test] fn empty_or_json_whitespace_only_response_document_fails_closed() { for raw in ["", " ", "\t\r\n"] { @@ -67,6 +89,7 @@ fn response_document_errors_are_deterministic_and_source_free() { for error in [ WebDriverBiDiResponseDocumentAdmissionError::EmptyDocument, WebDriverBiDiResponseDocumentAdmissionError::DocumentTooLarge, + WebDriverBiDiResponseDocumentAdmissionError::InvalidUtf8, WebDriverBiDiResponseDocumentAdmissionError::InvalidObjectBoundary, ] { assert!(!error.to_string().is_empty()); From 42c0443569ba32c8c6ea4fcb712b45e8eec444a2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 11:33:54 -0700 Subject: [PATCH 048/118] feat(core): admit bounded BiDi response bytes --- .../src/webdriver_bidi_response_document.rs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document.rs b/crates/originweave-core/src/webdriver_bidi_response_document.rs index 4eacf84cb..3bb7a42c4 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document.rs @@ -15,6 +15,8 @@ pub enum WebDriverBiDiResponseDocumentAdmissionError { EmptyDocument, /// The raw response exceeds the OriginWeave pre-parser byte budget. DocumentTooLarge, + /// The raw response is not valid UTF-8. + InvalidUtf8, /// The first and last non-whitespace bytes do not delimit a JSON object. InvalidObjectBoundary, } @@ -27,6 +29,9 @@ impl fmt::Display for WebDriverBiDiResponseDocumentAdmissionError { formatter, "WebDriver BiDi response document exceeds {MAX_WEBDRIVER_BIDI_RESPONSE_DOCUMENT_BYTES} bytes" ), + Self::InvalidUtf8 => { + formatter.write_str("WebDriver BiDi response document is not valid UTF-8") + } Self::InvalidObjectBoundary => formatter.write_str( "WebDriver BiDi response document must have a top-level JSON object boundary", ), @@ -68,6 +73,23 @@ impl BoundedWebDriverBiDiResponseDocument { }) } + /// Admits raw transport bytes after bounding them and validating UTF-8. + /// + /// The byte budget is checked before UTF-8 validation or owned-text + /// allocation. This keeps hostile transport payloads outside the parser + /// boundary until both the resource and text-encoding contracts hold. + pub fn from_utf8_bytes( + raw: &[u8], + ) -> Result { + if raw.len() > MAX_WEBDRIVER_BIDI_RESPONSE_DOCUMENT_BYTES { + return Err(WebDriverBiDiResponseDocumentAdmissionError::DocumentTooLarge); + } + + let raw = std::str::from_utf8(raw) + .map_err(|_| WebDriverBiDiResponseDocumentAdmissionError::InvalidUtf8)?; + Self::new(raw) + } + /// Returns the exact admitted response text, including surrounding JSON whitespace. #[must_use] pub fn as_str(&self) -> &str { From c80ffdb05c923971d182242255a8f1f8b8cc1286 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 11:57:08 -0700 Subject: [PATCH 049/118] test(core): require typed BiDi error-code evidence --- ...river_bidi_response_error_code_evidence.rs | 38 +++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_response_error_code_evidence.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_response_error_code_evidence.rs b/crates/originweave-core/tests/webdriver_bidi_response_error_code_evidence.rs new file mode 100644 index 000000000..e2761843a --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_response_error_code_evidence.rs @@ -0,0 +1,38 @@ +use std::error::Error; + +use originweave_core::{ + BoundedWebDriverBiDiResponseDocument, WebDriverBiDiErrorCode, +}; + +#[test] +fn parsed_error_envelope_retains_typed_error_code() -> Result<(), Box> { + for (raw_code, expected) in [ + ("invalid argument", WebDriverBiDiErrorCode::InvalidArgument), + ( + "no such client window", + WebDriverBiDiErrorCode::NoSuchClientWindow, + ), + ( + "unavailable network data", + WebDriverBiDiErrorCode::UnavailableNetworkData, + ), + ] { + let raw = format!( + "{{\"type\":\"error\",\"id\":7,\"error\":\"{raw_code}\",\"message\":\"remote failure\"}}" + ); + let parsed = BoundedWebDriverBiDiResponseDocument::new(&raw)?.parse_command_response()?; + assert_eq!(parsed.error_code(), Some(expected)); + } + Ok(()) +} + +#[test] +fn parsed_success_envelope_has_no_error_code() -> Result<(), Box> { + let parsed = BoundedWebDriverBiDiResponseDocument::new( + "{\"type\":\"success\",\"id\":7,\"result\":{}}", + )? + .parse_command_response()?; + + assert_eq!(parsed.error_code(), None); + Ok(()) +} From f331b32656a2d39e6ddcf49f83326583248a4298 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 12:01:58 -0700 Subject: [PATCH 050/118] style(core): canonicalize error-code RED --- .../tests/webdriver_bidi_response_error_code_evidence.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_response_error_code_evidence.rs b/crates/originweave-core/tests/webdriver_bidi_response_error_code_evidence.rs index e2761843a..6c3642dad 100644 --- a/crates/originweave-core/tests/webdriver_bidi_response_error_code_evidence.rs +++ b/crates/originweave-core/tests/webdriver_bidi_response_error_code_evidence.rs @@ -1,8 +1,6 @@ use std::error::Error; -use originweave_core::{ - BoundedWebDriverBiDiResponseDocument, WebDriverBiDiErrorCode, -}; +use originweave_core::{BoundedWebDriverBiDiResponseDocument, WebDriverBiDiErrorCode}; #[test] fn parsed_error_envelope_retains_typed_error_code() -> Result<(), Box> { From e9f4dcc3d17a23df93f96ce5f56b2daea34af8c5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 12:07:16 -0700 Subject: [PATCH 051/118] fix(core): retain typed BiDi error-code evidence --- crates/originweave-core/src/lib.rs | 1 + .../src/webdriver_bidi_error_code.rs | 187 ++++++++++++++---- .../src/webdriver_bidi_response_envelope.rs | 47 +++-- 3 files changed, 187 insertions(+), 48 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 8571f9d87..c5a825a9e 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -74,6 +74,7 @@ pub use webdriver_bidi_command::{ WebDriverBiDiLocateNodesResponseCorrelationError, WebDriverBiDiLocateNodesResponseEnvelopeError, }; +pub use webdriver_bidi_error_code::WebDriverBiDiErrorCode; pub use webdriver_bidi_response_document::{ BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_RESPONSE_DOCUMENT_BYTES, WebDriverBiDiResponseDocumentAdmissionError, diff --git a/crates/originweave-core/src/webdriver_bidi_error_code.rs b/crates/originweave-core/src/webdriver_bidi_error_code.rs index ffcea6a1b..d359fd048 100644 --- a/crates/originweave-core/src/webdriver_bidi_error_code.rs +++ b/crates/originweave-core/src/webdriver_bidi_error_code.rs @@ -1,38 +1,155 @@ -/// Returns whether `value` is one of the error codes admitted by the current WebDriver BiDi specification. -pub(crate) fn is_webdriver_bidi_error_code(value: &[u8]) -> bool { - const ERROR_CODES: &[&[u8]] = &[ - b"invalid argument", - b"invalid selector", - b"invalid session id", - b"invalid web extension", - b"move target out of bounds", - b"no such alert", - b"no such client window", - b"no such network collector", - b"no such element", - b"no such frame", - b"no such handle", - b"no such history entry", - b"no such intercept", - b"no such network data", - b"no such node", - b"no such request", - b"no such screencast", - b"no such script", - b"no such storage partition", - b"no such user context", - b"no such web extension", - b"session not created", - b"unable to capture screen", - b"unable to close browser", - b"unable to set cookie", - b"unable to set file input", - b"unavailable network data", - b"underspecified storage partition", - b"unknown command", - b"unknown error", - b"unsupported operation", +/// Typed current WebDriver BiDi protocol error code retained from one validated error response. +/// +/// This vocabulary is deliberately closed over the protocol error codes reviewed by OriginWeave. +/// Unknown wire text remains fail-closed and cannot become typed protocol evidence. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiErrorCode { + /// The command or one of its arguments is invalid. + InvalidArgument, + /// A selector argument is invalid. + InvalidSelector, + /// The referenced browser session does not exist. + InvalidSessionId, + /// The referenced web extension is invalid. + InvalidWebExtension, + /// A requested pointer move target is outside the allowed bounds. + MoveTargetOutOfBounds, + /// The referenced user prompt does not exist. + NoSuchAlert, + /// The referenced client window does not exist. + NoSuchClientWindow, + /// The referenced network collector does not exist. + NoSuchNetworkCollector, + /// The referenced element does not exist. + NoSuchElement, + /// The referenced frame does not exist. + NoSuchFrame, + /// The referenced handle does not exist. + NoSuchHandle, + /// The referenced history entry does not exist. + NoSuchHistoryEntry, + /// The referenced network intercept does not exist. + NoSuchIntercept, + /// The requested network data does not exist. + NoSuchNetworkData, + /// The referenced node does not exist. + NoSuchNode, + /// The referenced network request does not exist. + NoSuchRequest, + /// The referenced screencast does not exist. + NoSuchScreencast, + /// The referenced script does not exist. + NoSuchScript, + /// The referenced storage partition does not exist. + NoSuchStoragePartition, + /// The referenced user context does not exist. + NoSuchUserContext, + /// The referenced web extension does not exist. + NoSuchWebExtension, + /// A browser session could not be created. + SessionNotCreated, + /// The browser could not capture the requested screen image. + UnableToCaptureScreen, + /// The browser could not close as requested. + UnableToCloseBrowser, + /// The browser could not set the requested cookie. + UnableToSetCookie, + /// The browser could not set the requested file input. + UnableToSetFileInput, + /// Requested network data is temporarily unavailable. + UnavailableNetworkData, + /// The supplied storage-partition descriptor is underspecified. + UnderspecifiedStoragePartition, + /// The command is unknown to the remote end. + UnknownCommand, + /// The remote end reported an otherwise unclassified protocol error. + UnknownError, + /// The requested operation is unsupported by the remote end. + UnsupportedOperation, +} + +/// Parse one exact decoded WebDriver BiDi `ErrorCode` value into typed protocol evidence. +pub(crate) fn parse_webdriver_bidi_error_code(value: &[u8]) -> Option { + const ERROR_CODES: &[(&[u8], WebDriverBiDiErrorCode)] = &[ + (b"invalid argument", WebDriverBiDiErrorCode::InvalidArgument), + (b"invalid selector", WebDriverBiDiErrorCode::InvalidSelector), + (b"invalid session id", WebDriverBiDiErrorCode::InvalidSessionId), + (b"invalid web extension", WebDriverBiDiErrorCode::InvalidWebExtension), + ( + b"move target out of bounds", + WebDriverBiDiErrorCode::MoveTargetOutOfBounds, + ), + (b"no such alert", WebDriverBiDiErrorCode::NoSuchAlert), + ( + b"no such client window", + WebDriverBiDiErrorCode::NoSuchClientWindow, + ), + ( + b"no such network collector", + WebDriverBiDiErrorCode::NoSuchNetworkCollector, + ), + (b"no such element", WebDriverBiDiErrorCode::NoSuchElement), + (b"no such frame", WebDriverBiDiErrorCode::NoSuchFrame), + (b"no such handle", WebDriverBiDiErrorCode::NoSuchHandle), + ( + b"no such history entry", + WebDriverBiDiErrorCode::NoSuchHistoryEntry, + ), + (b"no such intercept", WebDriverBiDiErrorCode::NoSuchIntercept), + ( + b"no such network data", + WebDriverBiDiErrorCode::NoSuchNetworkData, + ), + (b"no such node", WebDriverBiDiErrorCode::NoSuchNode), + (b"no such request", WebDriverBiDiErrorCode::NoSuchRequest), + (b"no such screencast", WebDriverBiDiErrorCode::NoSuchScreencast), + (b"no such script", WebDriverBiDiErrorCode::NoSuchScript), + ( + b"no such storage partition", + WebDriverBiDiErrorCode::NoSuchStoragePartition, + ), + ( + b"no such user context", + WebDriverBiDiErrorCode::NoSuchUserContext, + ), + ( + b"no such web extension", + WebDriverBiDiErrorCode::NoSuchWebExtension, + ), + ( + b"session not created", + WebDriverBiDiErrorCode::SessionNotCreated, + ), + ( + b"unable to capture screen", + WebDriverBiDiErrorCode::UnableToCaptureScreen, + ), + ( + b"unable to close browser", + WebDriverBiDiErrorCode::UnableToCloseBrowser, + ), + (b"unable to set cookie", WebDriverBiDiErrorCode::UnableToSetCookie), + ( + b"unable to set file input", + WebDriverBiDiErrorCode::UnableToSetFileInput, + ), + ( + b"unavailable network data", + WebDriverBiDiErrorCode::UnavailableNetworkData, + ), + ( + b"underspecified storage partition", + WebDriverBiDiErrorCode::UnderspecifiedStoragePartition, + ), + (b"unknown command", WebDriverBiDiErrorCode::UnknownCommand), + (b"unknown error", WebDriverBiDiErrorCode::UnknownError), + ( + b"unsupported operation", + WebDriverBiDiErrorCode::UnsupportedOperation, + ), ]; - ERROR_CODES.contains(&value) + ERROR_CODES + .iter() + .find_map(|(raw, code)| (*raw == value).then_some(*code)) } diff --git a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs index 29e3e6728..cf22c762c 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_envelope.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_envelope.rs @@ -2,7 +2,8 @@ use std::{error::Error, fmt}; use crate::{ BoundedWebDriverBiDiResponseDocument, MAX_WEBDRIVER_BIDI_COMMAND_ID, - WebDriverBiDiCommandResponseKind, webdriver_bidi_error_code::is_webdriver_bidi_error_code, + WebDriverBiDiCommandResponseKind, + webdriver_bidi_error_code::{WebDriverBiDiErrorCode, parse_webdriver_bidi_error_code}, }; /// Maximum accepted JSON container nesting depth for one WebDriver BiDi response document. @@ -77,14 +78,16 @@ impl Error for WebDriverBiDiResponseEnvelopeParseError {} /// Typed evidence that one bounded raw document is a syntactically valid WebDriver BiDi command /// response envelope. /// -/// The value retains the exact admitted wire text and exposes only the command-response kind and -/// parsed response identifier needed by later correlation. Parsing does not authenticate a browser -/// or transport and does not grant browser, node, policy, or Agent authority. +/// The value retains the exact admitted wire text, command-response kind, parsed response +/// identifier, and the typed protocol error code for an error response. Parsing does not +/// authenticate a browser or transport and does not grant browser, node, policy, or Agent +/// authority. #[derive(Debug, PartialEq, Eq)] pub struct ParsedWebDriverBiDiCommandResponseEnvelope { document: BoundedWebDriverBiDiResponseDocument, kind: WebDriverBiDiCommandResponseKind, response_id: Option, + error_code: Option, } impl ParsedWebDriverBiDiCommandResponseEnvelope { @@ -101,6 +104,16 @@ impl ParsedWebDriverBiDiCommandResponseEnvelope { self.response_id } + /// Returns the typed protocol error code, or `None` for a success response. + /// + /// The value is derived from the same decoded top-level `error` field that passed complete + /// envelope validation; callers therefore do not need to reparse untrusted wire text merely to + /// classify a recoverable protocol failure. + #[must_use] + pub const fn error_code(&self) -> Option { + self.error_code + } + /// Returns the exact bounded wire text from which this envelope evidence was parsed. #[must_use] pub fn as_str(&self) -> &str { @@ -112,8 +125,9 @@ impl BoundedWebDriverBiDiResponseDocument { /// Parses this already-bounded raw document into typed command-response envelope evidence. /// /// Complete JSON syntax, decoded top-level field uniqueness, response-kind requirements, - /// protocol-range response identifiers, and explicit parser resource budgets are enforced - /// before the value can be used by a later correlation boundary. + /// protocol-range response identifiers, typed current error-code classification, and explicit + /// parser resource budgets are enforced before the value can be used by a later correlation + /// boundary. pub fn parse_command_response( self, ) -> Result @@ -123,6 +137,7 @@ impl BoundedWebDriverBiDiResponseDocument { document: self, kind: parsed.kind, response_id: parsed.response_id, + error_code: parsed.error_code, }) } } @@ -140,6 +155,7 @@ enum ParsedJsonValue { struct ParsedEnvelopeFields { kind: WebDriverBiDiCommandResponseKind, response_id: Option, + error_code: Option, } struct ResponseEnvelopeParser<'input> { @@ -218,9 +234,14 @@ impl<'input> ResponseEnvelopeParser<'input> { let kind = Self::parse_response_type(response_type)?; let response_id = Self::parse_response_id(response_id, kind)?; - Self::validate_required_payload(kind, result, error_code, message, stacktrace)?; + let error_code = + Self::validate_required_payload(kind, result, error_code, message, stacktrace)?; - Ok(ParsedEnvelopeFields { kind, response_id }) + Ok(ParsedEnvelopeFields { + kind, + response_id, + error_code, + }) } fn parse_response_type( @@ -268,7 +289,7 @@ impl<'input> ResponseEnvelopeParser<'input> { error_code: Option, message: Option, stacktrace: Option, - ) -> Result<(), WebDriverBiDiResponseEnvelopeParseError> { + ) -> Result, WebDriverBiDiResponseEnvelopeParseError> { match kind { WebDriverBiDiCommandResponseKind::Success => { let result = result @@ -278,6 +299,7 @@ impl<'input> ResponseEnvelopeParser<'input> { WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, ); } + Ok(None) } WebDriverBiDiCommandResponseKind::Error => { let error_code = error_code @@ -294,9 +316,8 @@ impl<'input> ResponseEnvelopeParser<'input> { WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, ); } - if !is_webdriver_bidi_error_code(&error_code) { - return Err(WebDriverBiDiResponseEnvelopeParseError::UnexpectedErrorCode); - } + let error_code = parse_webdriver_bidi_error_code(&error_code) + .ok_or(WebDriverBiDiResponseEnvelopeParseError::UnexpectedErrorCode)?; if let Some(stacktrace) = stacktrace && !matches!(stacktrace, ParsedJsonValue::String(_)) { @@ -304,9 +325,9 @@ impl<'input> ResponseEnvelopeParser<'input> { WebDriverBiDiResponseEnvelopeParseError::InvalidRequiredPayloadType, ); } + Ok(Some(error_code)) } } - Ok(()) } fn parse_value( From 358c6e27ea3d5c3da6476a6251ea5c89bd5c5480 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 12:11:36 -0700 Subject: [PATCH 052/118] test(core): prove every typed BiDi error mapping --- .../webdriver_bidi_response_error_code.rs | 126 ++++++++++++------ 1 file changed, 86 insertions(+), 40 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs index faed05b61..e90c8d732 100644 --- a/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs +++ b/crates/originweave-core/tests/webdriver_bidi_response_error_code.rs @@ -1,51 +1,97 @@ use std::error::Error; -use originweave_core::BoundedWebDriverBiDiResponseDocument; +use originweave_core::{BoundedWebDriverBiDiResponseDocument, WebDriverBiDiErrorCode}; -const CURRENT_WEBDRIVER_BIDI_ERROR_CODES: &[&str] = &[ - "invalid argument", - "invalid selector", - "invalid session id", - "invalid web extension", - "move target out of bounds", - "no such alert", - "no such client window", - "no such network collector", - "no such element", - "no such frame", - "no such handle", - "no such history entry", - "no such intercept", - "no such network data", - "no such node", - "no such request", - "no such screencast", - "no such script", - "no such storage partition", - "no such user context", - "no such web extension", - "session not created", - "unable to capture screen", - "unable to close browser", - "unable to set cookie", - "unable to set file input", - "unavailable network data", - "underspecified storage partition", - "unknown command", - "unknown error", - "unsupported operation", +const CURRENT_WEBDRIVER_BIDI_ERROR_CODES: &[(&str, WebDriverBiDiErrorCode)] = &[ + ("invalid argument", WebDriverBiDiErrorCode::InvalidArgument), + ("invalid selector", WebDriverBiDiErrorCode::InvalidSelector), + ("invalid session id", WebDriverBiDiErrorCode::InvalidSessionId), + ("invalid web extension", WebDriverBiDiErrorCode::InvalidWebExtension), + ( + "move target out of bounds", + WebDriverBiDiErrorCode::MoveTargetOutOfBounds, + ), + ("no such alert", WebDriverBiDiErrorCode::NoSuchAlert), + ( + "no such client window", + WebDriverBiDiErrorCode::NoSuchClientWindow, + ), + ( + "no such network collector", + WebDriverBiDiErrorCode::NoSuchNetworkCollector, + ), + ("no such element", WebDriverBiDiErrorCode::NoSuchElement), + ("no such frame", WebDriverBiDiErrorCode::NoSuchFrame), + ("no such handle", WebDriverBiDiErrorCode::NoSuchHandle), + ( + "no such history entry", + WebDriverBiDiErrorCode::NoSuchHistoryEntry, + ), + ("no such intercept", WebDriverBiDiErrorCode::NoSuchIntercept), + ( + "no such network data", + WebDriverBiDiErrorCode::NoSuchNetworkData, + ), + ("no such node", WebDriverBiDiErrorCode::NoSuchNode), + ("no such request", WebDriverBiDiErrorCode::NoSuchRequest), + ("no such screencast", WebDriverBiDiErrorCode::NoSuchScreencast), + ("no such script", WebDriverBiDiErrorCode::NoSuchScript), + ( + "no such storage partition", + WebDriverBiDiErrorCode::NoSuchStoragePartition, + ), + ( + "no such user context", + WebDriverBiDiErrorCode::NoSuchUserContext, + ), + ( + "no such web extension", + WebDriverBiDiErrorCode::NoSuchWebExtension, + ), + ( + "session not created", + WebDriverBiDiErrorCode::SessionNotCreated, + ), + ( + "unable to capture screen", + WebDriverBiDiErrorCode::UnableToCaptureScreen, + ), + ( + "unable to close browser", + WebDriverBiDiErrorCode::UnableToCloseBrowser, + ), + ("unable to set cookie", WebDriverBiDiErrorCode::UnableToSetCookie), + ( + "unable to set file input", + WebDriverBiDiErrorCode::UnableToSetFileInput, + ), + ( + "unavailable network data", + WebDriverBiDiErrorCode::UnavailableNetworkData, + ), + ( + "underspecified storage partition", + WebDriverBiDiErrorCode::UnderspecifiedStoragePartition, + ), + ("unknown command", WebDriverBiDiErrorCode::UnknownCommand), + ("unknown error", WebDriverBiDiErrorCode::UnknownError), + ( + "unsupported operation", + WebDriverBiDiErrorCode::UnsupportedOperation, + ), ]; #[test] -fn parser_accepts_current_webdriver_bidi_error_code_vocabulary() -> Result<(), Box> { - for error_code in CURRENT_WEBDRIVER_BIDI_ERROR_CODES { +fn parser_retains_every_current_webdriver_bidi_error_code() -> Result<(), Box> { + for &(raw_code, expected) in CURRENT_WEBDRIVER_BIDI_ERROR_CODES { let raw = format!( - "{{\"type\":\"error\",\"id\":7,\"error\":\"{error_code}\",\"message\":\"browser rejected command\"}}" + "{{\"type\":\"error\",\"id\":7,\"error\":\"{raw_code}\",\"message\":\"browser rejected command\"}}" ); - let parsed = BoundedWebDriverBiDiResponseDocument::new(&raw)?.parse_command_response(); - assert!( - parsed.is_ok(), - "current WebDriver BiDi error code must remain admissible: {error_code}" + let parsed = BoundedWebDriverBiDiResponseDocument::new(&raw)?.parse_command_response()?; + assert_eq!( + parsed.error_code(), + Some(expected), + "current WebDriver BiDi error code must retain its typed mapping: {raw_code}" ); } Ok(()) From 67403395de36cbb4368b0b370756f5990549058b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 13:12:23 -0700 Subject: [PATCH 053/118] fix(core): restore canonical Rust formatting --- .../src/webdriver_bidi_error_code.rs | 25 +++++++++++++++---- .../webdriver_bidi_response_error_code.rs | 20 ++++++++++++--- ...river_bidi_response_error_code_evidence.rs | 7 +++--- 3 files changed, 39 insertions(+), 13 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_error_code.rs b/crates/originweave-core/src/webdriver_bidi_error_code.rs index d359fd048..bd336cf2d 100644 --- a/crates/originweave-core/src/webdriver_bidi_error_code.rs +++ b/crates/originweave-core/src/webdriver_bidi_error_code.rs @@ -73,8 +73,14 @@ pub(crate) fn parse_webdriver_bidi_error_code(value: &[u8]) -> Option Option Option Result<(), Box #[test] fn parsed_success_envelope_has_no_error_code() -> Result<(), Box> { - let parsed = BoundedWebDriverBiDiResponseDocument::new( - "{\"type\":\"success\",\"id\":7,\"result\":{}}", - )? - .parse_command_response()?; + let parsed = + BoundedWebDriverBiDiResponseDocument::new("{\"type\":\"success\",\"id\":7,\"result\":{}}")? + .parse_command_response()?; assert_eq!(parsed.error_code(), None); Ok(()) From a4510546ce7ce8335325af836c2e844137241a87 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 14:07:24 -0700 Subject: [PATCH 054/118] test(core): require wire-to-authority node binding --- .../webdriver_bidi_wire_authority_binding.rs | 118 ++++++++++++++++++ 1 file changed, 118 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs b/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs new file mode 100644 index 000000000..3e7b39518 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs @@ -0,0 +1,118 @@ +use std::error::Error; + +use originweave_core::{ + BoundedWebDriverBiDiResponseDocument, BrowserAuthorityRegistry, BrowserContextDispatchTarget, + BrowserContextOriginDispatchTarget, BrowserContextOriginEpochDispatchTarget, + BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolKind, + BrowserRegistryError, Origin, OriginWeaveProtocolVersion, ValidatedBrowserProtocolUse, + WebDriverBiDiAccessibilityQuery, WebDriverBiDiLocateNodesAdmissionError, + WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesResponseDocumentError, +}; + +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"; + +fn locate_nodes_command() -> Result> { + let query = WebDriverBiDiAccessibilityQuery::new(Some("button"), Some("Submit task"), 1)?; + Ok(WebDriverBiDiLocateNodesCommand::new( + 42, + "context-a", + &query, + )?) +} + +fn controlled_origin() -> Result> { + Origin::parse("https://app.example").map_err(|_error| "valid controlled fixture origin".into()) +} + +fn current_target<'a>( + registry: &mut BrowserAuthorityRegistry, + origin: &'a Origin, + external_context: &str, +) -> Result, Box> { + let session = registry.register_session("webdriver-session")?; + let context = registry.register_context(session, external_context)?; + let epoch = registry.bind_context_origin(session, context, origin)?; + Ok(BrowserContextOriginEpochDispatchTarget::new( + BrowserContextOriginDispatchTarget::new( + BrowserContextDispatchTarget::new(session, context), + origin, + ), + epoch, + )) +} + +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 successful_wire_document() -> Result> { + Ok(BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":42,"result":{"nodes":[{"type":"node","sharedId":"node-a"}]}}"#, + )?) +} + +#[test] +fn wire_response_binds_nodes_to_exact_current_authority_without_caller_selected_intermediate_result( +) -> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let origin = controlled_origin()?; + let target = current_target(&mut registry, &origin, "context-a")?; + + let handles = locate_nodes_command()?.bind_response_document_nodes( + successful_wire_document()?, + semantic_observation_proof()?, + &mut registry, + target, + )?; + + assert_eq!(handles.len(), 1); + assert_eq!(handles[0].origin(), &origin); + assert_eq!(handles[0].document_epoch(), target.expected_epoch()); + Ok(()) +} + +#[test] +fn wire_response_binding_preserves_current_context_authority_failure() -> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let origin = controlled_origin()?; + let target = current_target(&mut registry, &origin, "context-b")?; + + let error = locate_nodes_command()?.bind_response_document_nodes( + successful_wire_document()?, + semantic_observation_proof()?, + &mut registry, + target, + ); + + assert_eq!( + error, + Err(WebDriverBiDiLocateNodesResponseDocumentError::NodeBinding( + WebDriverBiDiLocateNodesAdmissionError::BrowserAuthority( + BrowserRegistryError::ContextExternalIdentifierMismatch, + ), + )) + ); + let error = error.err().ok_or("expected exact current context failure")?; + assert!(error.source().is_some()); + assert!(!error.to_string().is_empty()); + Ok(()) +} From bcc719fa0992e9cd1b896416b85513ef5d41545c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 14:10:20 -0700 Subject: [PATCH 055/118] test(core): format wire authority regression --- .../tests/webdriver_bidi_wire_authority_binding.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs b/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs index 3e7b39518..e0e57b7e7 100644 --- a/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs +++ b/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs @@ -71,8 +71,8 @@ fn successful_wire_document() -> Result Result<(), Box> { +fn wire_response_binds_nodes_to_exact_current_authority_without_caller_selected_intermediate_result() +-> Result<(), Box> { let mut registry = BrowserAuthorityRegistry::new(); let origin = controlled_origin()?; let target = current_target(&mut registry, &origin, "context-a")?; @@ -91,7 +91,8 @@ fn wire_response_binds_nodes_to_exact_current_authority_without_caller_selected_ } #[test] -fn wire_response_binding_preserves_current_context_authority_failure() -> Result<(), Box> { +fn wire_response_binding_preserves_current_context_authority_failure() -> Result<(), Box> +{ let mut registry = BrowserAuthorityRegistry::new(); let origin = controlled_origin()?; let target = current_target(&mut registry, &origin, "context-b")?; @@ -111,7 +112,9 @@ fn wire_response_binding_preserves_current_context_authority_failure() -> Result ), )) ); - let error = error.err().ok_or("expected exact current context failure")?; + let error = error + .err() + .ok_or("expected exact current context failure")?; assert!(error.source().is_some()); assert!(!error.to_string().is_empty()); Ok(()) From fda7ea80a3275ca9411ee836f559dddb2d367bbf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 14:19:12 -0700 Subject: [PATCH 056/118] feat(core): bind wire locateNodes results to current authority --- ...iver_bidi_response_document_correlation.rs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 84635fa18..3b79bd319 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -12,6 +12,10 @@ use crate::webdriver_bidi_response_envelope::WebDriverBiDiResponseEnvelopeParseE use crate::webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; +use crate::{ + BrowserAuthorityRegistry, BrowserContextOriginEpochDispatchTarget, ObservedNodeHandle, + ValidatedBrowserProtocolUse, WebDriverBiDiLocateNodesAdmissionError, +}; /// Fail-closed errors while parsing, correlating, and admitting one bounded WebDriver BiDi /// `locateNodes` response document. @@ -39,6 +43,8 @@ pub enum WebDriverBiDiLocateNodesResponseDocumentError { InvalidResultNodeSharedId, /// Exact command-budget or remote-node admission rejected the wire-derived node batch. ResultAdmission(WebDriverBiDiLocateNodesResultAdmissionError), + /// Exact current browser authority rejected the wire-derived node batch. + NodeBinding(WebDriverBiDiLocateNodesAdmissionError), /// A second-pass result parser invariant failed after complete envelope parsing succeeded. ResultParserInvariant, } @@ -77,6 +83,10 @@ impl Display for WebDriverBiDiLocateNodesResponseDocumentError { formatter, "WebDriver BiDi locateNodes wire result rejected node admission: {error}" ), + Self::NodeBinding(error) => write!( + formatter, + "WebDriver BiDi locateNodes wire result rejected current browser authority: {error}" + ), Self::ResultParserInvariant => formatter.write_str( "WebDriver BiDi locateNodes result parser invariant failed after envelope validation", ), @@ -90,6 +100,7 @@ impl Error for WebDriverBiDiLocateNodesResponseDocumentError { Self::Parse(error) => Some(error), Self::Envelope(error) => Some(error), Self::ResultAdmission(error) => Some(error), + Self::NodeBinding(error) => Some(error), Self::MissingResultNodes | Self::InvalidResultNodes | Self::DuplicateResultNodes @@ -169,6 +180,30 @@ impl WebDriverBiDiLocateNodesCommand { .admit_result_nodes(&admission_parts) .map_err(WebDriverBiDiLocateNodesResponseDocumentError::ResultAdmission) } + + /// Consume one bounded raw `locateNodes` response through wire admission and current authority. + /// + /// This is the direct composition boundary from the exact parsed wire document to current + /// OriginWeave node authority. The caller cannot replace the response kind, response id, result + /// nodes, browsing-context identifier, or command result budget between wire parsing and node + /// binding. After wire-derived admission succeeds, the supplied WebDriver BiDi + /// `SemanticObservation` proof and exact current session/context/origin/document epoch are + /// revalidated by [`ValidatedWebDriverBiDiLocateNodesResult::bind_current_nodes`]. + /// + /// Success mints only [`ObservedNodeHandle`] values. It still does not authenticate Chromium, + /// ChromeDriver, WebSocket/TLS provenance, or the adapter process; authorize policy or typed + /// input; execute browser I/O; or prove an action post-condition. + pub fn bind_response_document_nodes( + self, + document: BoundedWebDriverBiDiResponseDocument, + validated: ValidatedBrowserProtocolUse, + authority_registry: &mut BrowserAuthorityRegistry, + target: BrowserContextOriginEpochDispatchTarget<'_>, + ) -> Result, WebDriverBiDiLocateNodesResponseDocumentError> { + self.admit_response_document_nodes(document)? + .bind_current_nodes(validated, authority_registry, target) + .map_err(WebDriverBiDiLocateNodesResponseDocumentError::NodeBinding) + } } #[cfg(test)] From b0c29ac48b66f81c0e59ce4abc625c04d1ddfd2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 17:29:47 -0700 Subject: [PATCH 057/118] test(core): cover BiDi bind admission failure --- .../webdriver_bidi_wire_authority_binding.rs | 37 ++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs b/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs index e0e57b7e7..70802e89c 100644 --- a/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs +++ b/crates/originweave-core/tests/webdriver_bidi_wire_authority_binding.rs @@ -6,7 +6,8 @@ use originweave_core::{ BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolKind, BrowserRegistryError, Origin, OriginWeaveProtocolVersion, ValidatedBrowserProtocolUse, WebDriverBiDiAccessibilityQuery, WebDriverBiDiLocateNodesAdmissionError, - WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesResponseDocumentError, + WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesResponseCorrelationError, + WebDriverBiDiLocateNodesResponseDocumentError, WebDriverBiDiLocateNodesResponseEnvelopeError, }; const ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion = @@ -70,6 +71,12 @@ fn successful_wire_document() -> Result Result> { + Ok(BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"success","id":43,"result":{"nodes":[{"type":"node","sharedId":"node-a"}]}}"#, + )?) +} + #[test] fn wire_response_binds_nodes_to_exact_current_authority_without_caller_selected_intermediate_result() -> Result<(), Box> { @@ -90,6 +97,34 @@ fn wire_response_binds_nodes_to_exact_current_authority_without_caller_selected_ Ok(()) } +#[test] +fn wire_response_binding_preserves_wire_correlation_failure_before_authority() +-> Result<(), Box> { + let mut registry = BrowserAuthorityRegistry::new(); + let origin = controlled_origin()?; + let target = current_target(&mut registry, &origin, "context-a")?; + + let error = locate_nodes_command()?.bind_response_document_nodes( + mismatched_wire_document()?, + semantic_observation_proof()?, + &mut registry, + target, + ); + + assert_eq!( + error, + Err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope( + WebDriverBiDiLocateNodesResponseEnvelopeError::Correlation( + WebDriverBiDiLocateNodesResponseCorrelationError::ResponseIdMismatch { + expected: 42, + actual: 43, + }, + ), + )) + ); + Ok(()) +} + #[test] fn wire_response_binding_preserves_current_context_authority_failure() -> Result<(), Box> { From 257d9a2cb7a978b04ee9edb686cf47ba5ec05232 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 18:29:50 -0700 Subject: [PATCH 058/118] test(core): require typed BiDi protocol errors --- ...driver_bidi_protocol_error_preservation.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs b/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs new file mode 100644 index 000000000..6393f1f71 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs @@ -0,0 +1,23 @@ +use std::error::Error; + +use originweave_core::{ + BoundedWebDriverBiDiResponseDocument, WebDriverBiDiAccessibilityQuery, WebDriverBiDiErrorCode, + WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesResponseDocumentError, +}; + +#[test] +fn correlated_wire_error_remains_typed_through_node_admission() -> Result<(), Box> { + let query = WebDriverBiDiAccessibilityQuery::new(Some("button"), Some("Submit task"), 4)?; + let command = WebDriverBiDiLocateNodesCommand::new(42, "context-a", &query)?; + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"error","id":42,"error":"unavailable network data","message":"retry later"}"#, + )?; + + assert_eq!( + command.admit_response_document_nodes(document), + Err(WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError( + WebDriverBiDiErrorCode::UnavailableNetworkData, + )) + ); + Ok(()) +} From ef4f55492a9cbafe21a34160ecc8d76e8e0f7dd5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 18:31:07 -0700 Subject: [PATCH 059/118] style(core): apply canonical rustfmt to BiDi RED --- .../tests/webdriver_bidi_protocol_error_preservation.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs b/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs index 6393f1f71..7b670d390 100644 --- a/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs +++ b/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs @@ -15,9 +15,11 @@ fn correlated_wire_error_remains_typed_through_node_admission() -> Result<(), Bo assert_eq!( command.admit_response_document_nodes(document), - Err(WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError( - WebDriverBiDiErrorCode::UnavailableNetworkData, - )) + Err( + WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError( + WebDriverBiDiErrorCode::UnavailableNetworkData, + ) + ) ); Ok(()) } From 544159479dd87e6d6ea6e05037d4d7313187b9dd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 18:34:55 -0700 Subject: [PATCH 060/118] fix(core): preserve correlated BiDi protocol errors --- ...iver_bidi_response_document_correlation.rs | 38 +++++++++++++------ 1 file changed, 26 insertions(+), 12 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 3b79bd319..1f4488812 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -14,17 +14,19 @@ use crate::webdriver_bidi_result::{ }; use crate::{ BrowserAuthorityRegistry, BrowserContextOriginEpochDispatchTarget, ObservedNodeHandle, - ValidatedBrowserProtocolUse, WebDriverBiDiLocateNodesAdmissionError, + ValidatedBrowserProtocolUse, WebDriverBiDiErrorCode, WebDriverBiDiLocateNodesAdmissionError, }; -/// Fail-closed errors while parsing, correlating, and admitting one bounded WebDriver BiDi -/// `locateNodes` response document. +/// Fail-closed errors while parsing, correlating, classifying, and admitting one bounded +/// WebDriver BiDi `locateNodes` response document. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WebDriverBiDiLocateNodesResponseDocumentError { /// The bounded document failed complete WebDriver BiDi response-envelope parsing. Parse(WebDriverBiDiResponseEnvelopeParseError), /// The parsed envelope failed exact command correlation or success-only conversion. Envelope(WebDriverBiDiLocateNodesResponseEnvelopeError), + /// The exactly correlated remote end returned a typed WebDriver BiDi protocol error. + ProtocolError(WebDriverBiDiErrorCode), /// The correlated success result omitted its required `nodes` field. MissingResultNodes, /// The correlated success result's `nodes` field was not a JSON array. @@ -60,6 +62,9 @@ impl Display for WebDriverBiDiLocateNodesResponseDocumentError { formatter, "WebDriver BiDi response document rejected command correlation: {error}" ), + Self::ProtocolError(_) => formatter.write_str( + "WebDriver BiDi response document contains a correlated typed protocol error", + ), Self::MissingResultNodes => { formatter.write_str("WebDriver BiDi locateNodes result is missing its nodes field") } @@ -101,7 +106,8 @@ impl Error for WebDriverBiDiLocateNodesResponseDocumentError { Self::Envelope(error) => Some(error), Self::ResultAdmission(error) => Some(error), Self::NodeBinding(error) => Some(error), - Self::MissingResultNodes + Self::ProtocolError(_) + | Self::MissingResultNodes | Self::InvalidResultNodes | Self::DuplicateResultNodes | Self::InvalidResultNode @@ -139,14 +145,17 @@ impl WebDriverBiDiLocateNodesCommand { /// Consume one bounded raw `locateNodes` response through exact wire-derived node admission. /// - /// The same bounded document first passes the complete response-envelope parser, exact command - /// correlation, and success-only conversion. Only then does the result parser derive the exact - /// `result.nodes` array from that already-validated wire document. The command's exact - /// `maxNodeCount` is carried into this parser so overflow items are consumed only as generic JSON - /// and produce the existing result-budget failure before authority-relevant node metadata is - /// decoded or normalized. Decoded duplicate `nodes`, and duplicate or malformed in-budget - /// `type`/`sharedId` fields, fail closed. JSON-escaped protocol metadata is decoded before - /// admission, and callers cannot supply replacement node metadata to this method. + /// The same bounded document first passes the complete response-envelope parser and exact + /// command correlation. An exactly correlated protocol error retains its reviewed typed error + /// code and stops before success-only result admission; unknown error-code text still fails + /// closed during complete envelope parsing. Only a correlated success proceeds to the result + /// parser, which derives the exact `result.nodes` array from that already-validated wire + /// document. The command's exact `maxNodeCount` is carried into this parser so overflow items are + /// consumed only as generic JSON and produce the existing result-budget failure before + /// authority-relevant node metadata is decoded or normalized. Decoded duplicate `nodes`, and + /// duplicate or malformed in-budget `type`/`sharedId` fields, fail closed. JSON-escaped protocol + /// metadata is decoded before admission, and callers cannot supply replacement node metadata to + /// this method. /// /// Success remains untrusted transport evidence. It does not authenticate Chromium, /// ChromeDriver, WebSocket/TLS provenance, or an adapter process; prove current @@ -165,6 +174,11 @@ impl WebDriverBiDiLocateNodesCommand { let correlated = self .correlate_response_envelope(parsed.kind(), parsed.response_id()) .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope)?; + if let Some(error_code) = parsed.error_code() { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError( + error_code, + )); + } let validated = correlated .into_validated_success() .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope)?; From cdf0d8fcd777c7ac91fdd47d6c82611328a8a8f8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 18:36:45 -0700 Subject: [PATCH 061/118] style(core): apply canonical rustfmt to BiDi error preservation --- .../src/webdriver_bidi_response_document_correlation.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 1f4488812..3dfab229e 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -175,9 +175,7 @@ impl WebDriverBiDiLocateNodesCommand { .correlate_response_envelope(parsed.kind(), parsed.response_id()) .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope)?; if let Some(error_code) = parsed.error_code() { - return Err(WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError( - error_code, - )); + return Err(WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError(error_code)); } let validated = correlated .into_validated_success() From d883f9a2fb8dbf29ca4ed7d9665ae42174859059 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 18:38:52 -0700 Subject: [PATCH 062/118] test(core): align BiDi wire error regression with typed protocol preservation --- .../webdriver_bidi_locate_nodes_wire_result.rs | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs index ce5d7bcd3..c61a17dff 100644 --- a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs @@ -1,7 +1,7 @@ use std::error::Error; use originweave_core::{ - BoundedWebDriverBiDiResponseDocument, WebDriverBiDiAccessibilityQuery, + BoundedWebDriverBiDiResponseDocument, WebDriverBiDiAccessibilityQuery, WebDriverBiDiErrorCode, WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesResponseDocumentError, }; @@ -138,7 +138,7 @@ fn wire_result_decodes_json_escaped_protocol_fields_before_admission() -> Result } #[test] -fn wire_result_boundary_preserves_parse_correlation_and_success_only_failures() +fn wire_result_boundary_preserves_parse_correlation_and_protocol_failures() -> Result<(), Box> { let malformed = BoundedWebDriverBiDiResponseDocument::new(r#"{"type":"success","id":42,"result":{},}"#)?; @@ -158,10 +158,12 @@ fn wire_result_boundary_preserves_parse_correlation_and_success_only_failures() let error_response = BoundedWebDriverBiDiResponseDocument::new( r#"{"type":"error","id":42,"error":"invalid argument","message":"bad request"}"#, )?; - assert!(matches!( + assert_eq!( locate_nodes_command(42, 1)?.admit_response_document_nodes(error_response), - Err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope(_)) - )); + Err(WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError( + WebDriverBiDiErrorCode::InvalidArgument, + )) + ); Ok(()) } @@ -169,6 +171,9 @@ fn wire_result_boundary_preserves_parse_correlation_and_success_only_failures() fn wire_result_document_error_display_and_sources_cover_result_failure_variants() -> Result<(), Box> { let source_free = [ + WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError( + WebDriverBiDiErrorCode::InvalidArgument, + ), WebDriverBiDiLocateNodesResponseDocumentError::MissingResultNodes, WebDriverBiDiLocateNodesResponseDocumentError::InvalidResultNodes, WebDriverBiDiLocateNodesResponseDocumentError::DuplicateResultNodes, From 910c519f355fc1ce31fc5f930c70d09b7dd5b052 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 18:41:00 -0700 Subject: [PATCH 063/118] style(core): apply canonical rustfmt to BiDi wire regression --- .../tests/webdriver_bidi_locate_nodes_wire_result.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs index c61a17dff..aba667210 100644 --- a/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs +++ b/crates/originweave-core/tests/webdriver_bidi_locate_nodes_wire_result.rs @@ -160,9 +160,11 @@ fn wire_result_boundary_preserves_parse_correlation_and_protocol_failures() )?; assert_eq!( locate_nodes_command(42, 1)?.admit_response_document_nodes(error_response), - Err(WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError( - WebDriverBiDiErrorCode::InvalidArgument, - )) + Err( + WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError( + WebDriverBiDiErrorCode::InvalidArgument, + ) + ) ); Ok(()) } From 291bbacdc4c3e8dcc8662c574c732b64971280e6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 18:49:02 -0700 Subject: [PATCH 064/118] fix(core): remove unreachable BiDi success-conversion edge --- ...iver_bidi_response_document_correlation.rs | 40 +++++++++++-------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs index 3dfab229e..068bdced5 100644 --- a/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs +++ b/crates/originweave-core/src/webdriver_bidi_response_document_correlation.rs @@ -146,16 +146,17 @@ impl WebDriverBiDiLocateNodesCommand { /// Consume one bounded raw `locateNodes` response through exact wire-derived node admission. /// /// The same bounded document first passes the complete response-envelope parser and exact - /// command correlation. An exactly correlated protocol error retains its reviewed typed error - /// code and stops before success-only result admission; unknown error-code text still fails - /// closed during complete envelope parsing. Only a correlated success proceeds to the result - /// parser, which derives the exact `result.nodes` array from that already-validated wire - /// document. The command's exact `maxNodeCount` is carried into this parser so overflow items are - /// consumed only as generic JSON and produce the existing result-budget failure before - /// authority-relevant node metadata is decoded or normalized. Decoded duplicate `nodes`, and - /// duplicate or malformed in-budget `type`/`sharedId` fields, fail closed. JSON-escaped protocol - /// metadata is decoded before admission, and callers cannot supply replacement node metadata to - /// this method. + /// command-id correlation. A parsed error with JSON `null` id remains uncorrelatable and fails + /// closed before its typed error code can influence recovery. An exactly correlated protocol + /// error retains its reviewed typed error code and stops before result admission; unknown + /// error-code text still fails closed during complete envelope parsing. Only a correlated + /// success proceeds to the result parser, which derives the exact `result.nodes` array from that + /// already-validated wire document. The command's exact `maxNodeCount` is carried into this + /// parser so overflow items are consumed only as generic JSON and produce the existing + /// result-budget failure before authority-relevant node metadata is decoded or normalized. + /// Decoded duplicate `nodes`, and duplicate or malformed in-budget `type`/`sharedId` fields, + /// fail closed. JSON-escaped protocol metadata is decoded before admission, and callers cannot + /// supply replacement node metadata to this method. /// /// Success remains untrusted transport evidence. It does not authenticate Chromium, /// ChromeDriver, WebSocket/TLS provenance, or an adapter process; prove current @@ -171,15 +172,22 @@ impl WebDriverBiDiLocateNodesCommand { let parsed = document .parse_command_response() .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Parse)?; - let correlated = self - .correlate_response_envelope(parsed.kind(), parsed.response_id()) - .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope)?; + let response_id = match parsed.response_id() { + Some(response_id) => response_id, + None => { + return Err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope( + WebDriverBiDiLocateNodesResponseEnvelopeError::UncorrelatableErrorResponse, + )); + } + }; + let validated = self.correlate_response_id(response_id).map_err(|error| { + WebDriverBiDiLocateNodesResponseDocumentError::Envelope( + WebDriverBiDiLocateNodesResponseEnvelopeError::Correlation(error), + ) + })?; if let Some(error_code) = parsed.error_code() { return Err(WebDriverBiDiLocateNodesResponseDocumentError::ProtocolError(error_code)); } - let validated = correlated - .into_validated_success() - .map_err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope)?; let wire_nodes = locate_nodes_result_document::parse_wire_locate_nodes_result_bounded( parsed.as_str(), validated.max_node_count(), From a11b838823b185829267ea0c3cccbcaeb0793f2c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:07:37 -0700 Subject: [PATCH 065/118] test(core): cover nullable BiDi error admission --- ...driver_bidi_protocol_error_preservation.rs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs b/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs index 7b670d390..a138cab4e 100644 --- a/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs +++ b/crates/originweave-core/tests/webdriver_bidi_protocol_error_preservation.rs @@ -3,6 +3,7 @@ use std::error::Error; use originweave_core::{ BoundedWebDriverBiDiResponseDocument, WebDriverBiDiAccessibilityQuery, WebDriverBiDiErrorCode, WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesResponseDocumentError, + WebDriverBiDiLocateNodesResponseEnvelopeError, }; #[test] @@ -23,3 +24,21 @@ fn correlated_wire_error_remains_typed_through_node_admission() -> Result<(), Bo ); Ok(()) } + +#[test] +fn nullable_wire_error_remains_uncorrelatable_before_protocol_error_admission() +-> Result<(), Box> { + let query = WebDriverBiDiAccessibilityQuery::new(Some("button"), Some("Submit task"), 4)?; + let command = WebDriverBiDiLocateNodesCommand::new(42, "context-a", &query)?; + let document = BoundedWebDriverBiDiResponseDocument::new( + r#"{"type":"error","id":null,"error":"invalid argument","message":"bad request"}"#, + )?; + + assert_eq!( + command.admit_response_document_nodes(document), + Err(WebDriverBiDiLocateNodesResponseDocumentError::Envelope( + WebDriverBiDiLocateNodesResponseEnvelopeError::UncorrelatableErrorResponse, + )) + ); + Ok(()) +} From 2687529751d59d5f5be14011df82ecc176898233 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:37:00 -0700 Subject: [PATCH 066/118] test(core): require bounded BiDi WebSocket endpoint admission --- .../webdriver_bidi_websocket_endpoint.rs | 168 ++++++++++++++++++ 1 file changed, 168 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs new file mode 100644 index 000000000..6a75f81da --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -0,0 +1,168 @@ +use std::error::Error; + +use originweave_core::{ + MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, WebDriverBiDiWebSocketEndpoint, + WebDriverBiDiWebSocketEndpointAdmissionError, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +#[test] +fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority() { + let ipv4 = WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://127.0.0.1:9515/session/{SESSION_ID}" + )) + .unwrap(); + assert!(!ipv4.is_secure()); + assert_eq!(ipv4.host(), "127.0.0.1"); + assert_eq!(ipv4.port(), 9515); + assert_eq!(ipv4.session_id(), SESSION_ID); + assert_eq!( + ipv4.as_str(), + format!("ws://127.0.0.1:9515/session/{SESSION_ID}") + ); + + let localhost = WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://localhost:4444/session/{SESSION_ID}" + )) + .unwrap(); + assert_eq!(localhost.host(), "localhost"); + + let ipv6 = WebDriverBiDiWebSocketEndpoint::new(&format!( + "wss://[::1]:9222/session/{SESSION_ID}" + )) + .unwrap(); + assert!(ipv6.is_secure()); + assert_eq!(ipv6.host(), "::1"); + assert_eq!(ipv6.port(), 9222); +} + +#[test] +fn remote_or_ambiguous_authorities_fail_closed() { + for endpoint in [ + format!("ws://example.com:9515/session/{SESSION_ID}"), + format!("ws://192.0.2.1:9515/session/{SESSION_ID}"), + format!("ws://[2001:db8::1]:9515/session/{SESSION_ID}"), + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost + ); + } + + for endpoint in [ + format!("ws://user@localhost:9515/session/{SESSION_ID}"), + format!("ws://localhost/session/{SESSION_ID}"), + format!("ws://::1:9515/session/{SESSION_ID}"), + format!("ws://[::1]9515/session/{SESSION_ID}"), + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority + ); + } +} + +#[test] +fn port_and_session_resource_are_canonical_and_bounded() { + for endpoint in [ + format!("ws://localhost:0/session/{SESSION_ID}"), + format!("ws://localhost:09515/session/{SESSION_ID}"), + format!("ws://localhost:65536/session/{SESSION_ID}"), + format!("ws://localhost:+9515/session/{SESSION_ID}"), + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort + ); + } + + for endpoint in [ + format!("ws://localhost:9515/other/{SESSION_ID}"), + format!("ws://localhost:9515/session/{SESSION_ID}/extra"), + "ws://localhost:9515/session/".to_owned(), + "ws://localhost:9515".to_owned(), + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource + ); + } + + for session_id in [ + "01234567-89ab-cdef-0123-456789abcdeF", + "0123456789ab-cdef-0123-456789abcdef", + "01234567-89ab-cdef-0123-456789abcdeg", + ] { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://localhost:9515/session/{session_id}" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId + ); + } +} + +#[test] +fn endpoint_text_rejects_noncanonical_or_unbounded_inputs_before_transport_use() { + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new("").unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint + ); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "http://localhost:9515/session/{SESSION_ID}" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme + ); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://local host:9515/session/{SESSION_ID}" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText + ); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://localhost:9515/session/{SESSION_ID}?token=secret" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden + ); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://localhost:9515/session/{SESSION_ID}#fragment" + )) + .unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden + ); + + let oversized = "x".repeat(MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES + 1); + assert_eq!( + WebDriverBiDiWebSocketEndpoint::new(&oversized).unwrap_err(), + WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong + ); +} + +#[test] +fn endpoint_error_contract_is_deterministic_and_source_free() { + let errors = [ + WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint, + WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme, + WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority, + WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource, + WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId, + ]; + + for error in errors { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_none()); + } +} From a1c4cb55609e03e76c8663b47727e302313cd08d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:40:16 -0700 Subject: [PATCH 067/118] style(core): canonicalize BiDi WebSocket endpoint RED --- .../webdriver_bidi_websocket_endpoint.rs | 132 ++++++++++-------- 1 file changed, 71 insertions(+), 61 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index 6a75f81da..a5954b32d 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -9,10 +9,12 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; #[test] fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority() { - let ipv4 = WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://127.0.0.1:9515/session/{SESSION_ID}" - )) - .unwrap(); + let ipv4_result = + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://127.0.0.1:9515/session/{SESSION_ID}")); + assert!(ipv4_result.is_ok(), "{ipv4_result:?}"); + let Ok(ipv4) = ipv4_result else { + return; + }; assert!(!ipv4.is_secure()); assert_eq!(ipv4.host(), "127.0.0.1"); assert_eq!(ipv4.port(), 9515); @@ -22,16 +24,20 @@ fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority( format!("ws://127.0.0.1:9515/session/{SESSION_ID}") ); - let localhost = WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://localhost:4444/session/{SESSION_ID}" - )) - .unwrap(); + let localhost_result = + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://localhost:4444/session/{SESSION_ID}")); + assert!(localhost_result.is_ok(), "{localhost_result:?}"); + let Ok(localhost) = localhost_result else { + return; + }; assert_eq!(localhost.host(), "localhost"); - let ipv6 = WebDriverBiDiWebSocketEndpoint::new(&format!( - "wss://[::1]:9222/session/{SESSION_ID}" - )) - .unwrap(); + let ipv6_result = + WebDriverBiDiWebSocketEndpoint::new(&format!("wss://[::1]:9222/session/{SESSION_ID}")); + assert!(ipv6_result.is_ok(), "{ipv6_result:?}"); + let Ok(ipv6) = ipv6_result else { + return; + }; assert!(ipv6.is_secure()); assert_eq!(ipv6.host(), "::1"); assert_eq!(ipv6.port(), 9222); @@ -44,10 +50,10 @@ fn remote_or_ambiguous_authorities_fail_closed() { format!("ws://192.0.2.1:9515/session/{SESSION_ID}"), format!("ws://[2001:db8::1]:9515/session/{SESSION_ID}"), ] { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost) + )); } for endpoint in [ @@ -55,11 +61,13 @@ fn remote_or_ambiguous_authorities_fail_closed() { format!("ws://localhost/session/{SESSION_ID}"), format!("ws://::1:9515/session/{SESSION_ID}"), format!("ws://[::1]9515/session/{SESSION_ID}"), + format!("ws://[::zz]:9515/session/{SESSION_ID}"), + format!("ws://:9515/session/{SESSION_ID}"), ] { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority) + )); } } @@ -71,10 +79,10 @@ fn port_and_session_resource_are_canonical_and_bounded() { format!("ws://localhost:65536/session/{SESSION_ID}"), format!("ws://localhost:+9515/session/{SESSION_ID}"), ] { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort) + )); } for endpoint in [ @@ -83,67 +91,69 @@ fn port_and_session_resource_are_canonical_and_bounded() { "ws://localhost:9515/session/".to_owned(), "ws://localhost:9515".to_owned(), ] { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&endpoint).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource) + )); } for session_id in [ "01234567-89ab-cdef-0123-456789abcdeF", "0123456789ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdeg", + "01234567_89ab-cdef-0123-456789abcdef", ] { - assert_eq!( + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "ws://localhost:9515/session/{session_id}" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId - ); + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId) + )); } } #[test] fn endpoint_text_rejects_noncanonical_or_unbounded_inputs_before_transport_use() { - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new("").unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint - ); - assert_eq!( + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(""), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint) + )); + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "http://localhost:9515/session/{SESSION_ID}" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme - ); - assert_eq!( + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme) + )); + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "ws://local host:9515/session/{SESSION_ID}" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText - ); - assert_eq!( + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText) + )); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://locálhost:9515/session/{SESSION_ID}" + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText) + )); + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "ws://localhost:9515/session/{SESSION_ID}?token=secret" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden - ); - assert_eq!( + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden) + )); + assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( "ws://localhost:9515/session/{SESSION_ID}#fragment" - )) - .unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden - ); + )), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden) + )); let oversized = "x".repeat(MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES + 1); - assert_eq!( - WebDriverBiDiWebSocketEndpoint::new(&oversized).unwrap_err(), - WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong - ); + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&oversized), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong) + )); } #[test] From dbdff70a1e8a045ee2735d86a321992e9ee295c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:41:25 -0700 Subject: [PATCH 068/118] style(core): apply canonical rustfmt to endpoint RED --- .../tests/webdriver_bidi_websocket_endpoint.rs | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index a5954b32d..fb47c4f3e 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -119,21 +119,15 @@ fn endpoint_text_rejects_noncanonical_or_unbounded_inputs_before_transport_use() Err(WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint) )); assert!(matches!( - WebDriverBiDiWebSocketEndpoint::new(&format!( - "http://localhost:9515/session/{SESSION_ID}" - )), + WebDriverBiDiWebSocketEndpoint::new(&format!("http://localhost:9515/session/{SESSION_ID}")), Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme) )); assert!(matches!( - WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://local host:9515/session/{SESSION_ID}" - )), + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://local host:9515/session/{SESSION_ID}")), Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText) )); assert!(matches!( - WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://locálhost:9515/session/{SESSION_ID}" - )), + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://locálhost:9515/session/{SESSION_ID}")), Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText) )); assert!(matches!( From 3d2c408aa18a5328aac283be5744d5f39633184f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:44:16 -0700 Subject: [PATCH 069/118] feat(core): admit bounded BiDi WebSocket endpoints --- crates/originweave-core/src/lib.rs | 5 + .../src/webdriver_bidi_websocket_endpoint.rs | 230 ++++++++++++++++++ 2 files changed, 235 insertions(+) create mode 100644 crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index c5a825a9e..67753ed6c 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -37,6 +37,7 @@ mod webdriver_bidi_response_document; mod webdriver_bidi_response_document_correlation; mod webdriver_bidi_response_envelope; mod webdriver_bidi_result; +mod webdriver_bidi_websocket_endpoint; pub use browser_authority_registry::BrowserAuthorityRegistry; pub use browser_protocol::{ @@ -87,3 +88,7 @@ pub use webdriver_bidi_response_envelope::{ pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; +pub use webdriver_bidi_websocket_endpoint::{ + MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, WebDriverBiDiWebSocketEndpoint, + WebDriverBiDiWebSocketEndpointAdmissionError, +}; diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs new file mode 100644 index 000000000..3042f97cd --- /dev/null +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -0,0 +1,230 @@ +use std::fmt; +use std::net::{Ipv4Addr, Ipv6Addr}; + +/// Maximum admitted bytes for one WebDriver BiDi WebSocket endpoint. +/// +/// This is an OriginWeave first-Chromium-fixture safety budget, not a +/// WebDriver BiDi protocol maximum. +pub const MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES: usize = 2_048; + +/// One bounded canonical WebDriver BiDi session WebSocket endpoint. +/// +/// This value is transport metadata only. Construction does not authenticate +/// Chromium, ChromeDriver, the operating-system peer, TLS, policy, or Agent +/// authority. The first real-Chromium fixture intentionally admits only +/// loopback listener identities; the connection boundary must still verify the +/// actual peer before exposing transport I/O. +#[derive(Debug, PartialEq, Eq)] +pub struct WebDriverBiDiWebSocketEndpoint { + endpoint: String, + secure: bool, + host: String, + port: u16, + session_id: String, +} + +impl WebDriverBiDiWebSocketEndpoint { + /// Admit one bounded canonical first-fixture WebDriver BiDi endpoint. + pub fn new(value: &str) -> Result { + if value.is_empty() { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::EmptyEndpoint); + } + if value.len() > MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::EndpointTooLong); + } + if value.bytes().any(|byte| !byte.is_ascii_graphic()) { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidEndpointText); + } + if value.bytes().any(|byte| matches!(byte, b'?' | b'#')) { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::QueryOrFragmentForbidden); + } + + let (secure, remainder) = if let Some(remainder) = value.strip_prefix("ws://") { + (false, remainder) + } else if let Some(remainder) = value.strip_prefix("wss://") { + (true, remainder) + } else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidScheme); + }; + + let Some(path_start) = remainder.find('/') else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource); + }; + let authority = &remainder[..path_start]; + let resource = &remainder[path_start..]; + if authority.is_empty() || authority.contains('@') { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + + let (host, port_text) = if let Some(bracketed) = authority.strip_prefix('[') { + let Some(close) = bracketed.find(']') else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + }; + let host_text = &bracketed[..close]; + let suffix = &bracketed[close + 1..]; + let Some(port_text) = suffix.strip_prefix(':') else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + }; + if port_text.is_empty() { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + let Ok(ip) = host_text.parse::() else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + }; + if !ip.is_loopback() { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost); + } + if ip.to_string() != host_text { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + (host_text.to_owned(), port_text) + } else { + let Some((host_text, port_text)) = authority.rsplit_once(':') else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + }; + if host_text.is_empty() || port_text.is_empty() || host_text.contains(':') { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + if host_text == "localhost" { + (host_text.to_owned(), port_text) + } else if let Ok(ip) = host_text.parse::() { + if ip.to_string() != host_text { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + if !ip.is_loopback() { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost); + } + (host_text.to_owned(), port_text) + } else if host_text + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'.' | b'-')) + { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost); + } else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); + } + }; + + let Ok(port) = port_text.parse::() else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort); + }; + if port == 0 || port.to_string() != port_text { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidPort); + } + + let Some(session_id) = resource.strip_prefix("/session/") else { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource); + }; + if session_id.is_empty() || session_id.contains('/') { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource); + } + if !is_canonical_session_uuid(session_id) { + return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId); + } + + Ok(Self { + endpoint: value.to_owned(), + secure, + host, + port, + session_id: session_id.to_owned(), + }) + } + + /// Return the exact admitted endpoint text. + #[must_use] + pub fn as_str(&self) -> &str { + &self.endpoint + } + + /// Return whether the endpoint uses `wss` rather than `ws`. + #[must_use] + pub const fn is_secure(&self) -> bool { + self.secure + } + + /// Return the canonical loopback listener host without IPv6 brackets. + #[must_use] + pub fn host(&self) -> &str { + &self.host + } + + /// Return the explicit nonzero listener port. + #[must_use] + pub const fn port(&self) -> u16 { + self.port + } + + /// Return the canonical lower-case UUID session identifier. + #[must_use] + pub fn session_id(&self) -> &str { + &self.session_id + } +} + +fn is_canonical_session_uuid(value: &str) -> bool { + let bytes = value.as_bytes(); + if bytes.len() != 36 { + return false; + } + for (index, byte) in bytes.iter().copied().enumerate() { + let valid = if matches!(index, 8 | 13 | 18 | 23) { + byte == b'-' + } else { + byte.is_ascii_digit() || matches!(byte, b'a'..=b'f') + }; + if !valid { + return false; + } + } + true +} + +/// Fail-closed admission errors for WebDriver BiDi WebSocket endpoint metadata. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiWebSocketEndpointAdmissionError { + /// The endpoint text is empty. + EmptyEndpoint, + /// The endpoint text exceeds the OriginWeave safety budget. + EndpointTooLong, + /// The endpoint contains non-ASCII, whitespace, or control text. + InvalidEndpointText, + /// The endpoint does not use the exact `ws` or `wss` scheme. + InvalidScheme, + /// Query or fragment data is present and therefore not part of the admitted session resource. + QueryOrFragmentForbidden, + /// The authority is absent, credential-bearing, ambiguous, or malformed. + InvalidAuthority, + /// The authority identifies a non-loopback host. + NonLoopbackHost, + /// The port is absent, zero, out of range, or not canonically serialized. + InvalidPort, + /// The path is not exactly one `/session/` resource. + InvalidSessionResource, + /// The session id is not one canonical lower-case UUID representation. + InvalidSessionId, +} + +impl fmt::Display for WebDriverBiDiWebSocketEndpointAdmissionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::EmptyEndpoint => "WebDriver BiDi WebSocket endpoint is empty", + Self::EndpointTooLong => "WebDriver BiDi WebSocket endpoint exceeds the safety budget", + Self::InvalidEndpointText => "WebDriver BiDi WebSocket endpoint text is not canonical ASCII", + Self::InvalidScheme => "WebDriver BiDi WebSocket endpoint scheme is not ws or wss", + Self::QueryOrFragmentForbidden => { + "WebDriver BiDi WebSocket endpoint query or fragment is forbidden" + } + Self::InvalidAuthority => "WebDriver BiDi WebSocket endpoint authority is invalid", + Self::NonLoopbackHost => "WebDriver BiDi WebSocket endpoint host is not loopback", + Self::InvalidPort => "WebDriver BiDi WebSocket endpoint port is invalid", + Self::InvalidSessionResource => { + "WebDriver BiDi WebSocket endpoint session resource is invalid" + } + Self::InvalidSessionId => "WebDriver BiDi WebSocket endpoint session id is invalid", + }; + f.write_str(message) + } +} + +impl std::error::Error for WebDriverBiDiWebSocketEndpointAdmissionError {} From e0ec37e97007fc6d71b3ca6956151bc5bc81b705 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:45:52 -0700 Subject: [PATCH 070/118] style(core): apply canonical endpoint rustfmt --- .../originweave-core/src/webdriver_bidi_websocket_endpoint.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 3042f97cd..2b266c679 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -210,7 +210,9 @@ impl fmt::Display for WebDriverBiDiWebSocketEndpointAdmissionError { let message = match self { Self::EmptyEndpoint => "WebDriver BiDi WebSocket endpoint is empty", Self::EndpointTooLong => "WebDriver BiDi WebSocket endpoint exceeds the safety budget", - Self::InvalidEndpointText => "WebDriver BiDi WebSocket endpoint text is not canonical ASCII", + Self::InvalidEndpointText => { + "WebDriver BiDi WebSocket endpoint text is not canonical ASCII" + } Self::InvalidScheme => "WebDriver BiDi WebSocket endpoint scheme is not ws or wss", Self::QueryOrFragmentForbidden => { "WebDriver BiDi WebSocket endpoint query or fragment is forbidden" From bebb1e1ad1aad847caddd9080a3888b24246c834 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:06:52 -0700 Subject: [PATCH 071/118] test(core): cover malformed BiDi endpoint authorities --- .../tests/webdriver_bidi_websocket_endpoint.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index fb47c4f3e..7dca568e5 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -71,6 +71,22 @@ fn remote_or_ambiguous_authorities_fail_closed() { } } +#[test] +fn malformed_loopback_authority_edge_cases_fail_closed() { + for endpoint in [ + format!("ws://[::1:9515/session/{SESSION_ID}"), + format!("ws://[::1]:/session/{SESSION_ID}"), + format!("ws://[0:0:0:0:0:0:0:1]:9515/session/{SESSION_ID}"), + format!("ws://localhost:/session/{SESSION_ID}"), + format!("ws://local_host:9515/session/{SESSION_ID}"), + ] { + assert!(matches!( + WebDriverBiDiWebSocketEndpoint::new(&endpoint), + Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority) + )); + } +} + #[test] fn port_and_session_resource_are_canonical_and_bounded() { for endpoint in [ From 805412826ff7e131fcb2e433be8eefbe62778166 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:08:18 -0700 Subject: [PATCH 072/118] fix(core): remove unreachable IPv4 endpoint branch --- .../originweave-core/src/webdriver_bidi_websocket_endpoint.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 2b266c679..3a555493b 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -88,9 +88,6 @@ impl WebDriverBiDiWebSocketEndpoint { if host_text == "localhost" { (host_text.to_owned(), port_text) } else if let Ok(ip) = host_text.parse::() { - if ip.to_string() != host_text { - return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidAuthority); - } if !ip.is_loopback() { return Err(WebDriverBiDiWebSocketEndpointAdmissionError::NonLoopbackHost); } From 42bfbed8a7268aaf60c5be3dc1f06b7df74107c3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:16:19 -0700 Subject: [PATCH 073/118] test(core): cover empty BiDi endpoint authority --- .../tests/webdriver_bidi_websocket_endpoint.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index 7dca568e5..3549d293b 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -57,6 +57,7 @@ fn remote_or_ambiguous_authorities_fail_closed() { } for endpoint in [ + format!("ws:///session/{SESSION_ID}"), format!("ws://user@localhost:9515/session/{SESSION_ID}"), format!("ws://localhost/session/{SESSION_ID}"), format!("ws://::1:9515/session/{SESSION_ID}"), @@ -185,4 +186,4 @@ fn endpoint_error_contract_is_deterministic_and_source_free() { assert!(!error.to_string().is_empty()); assert!(error.source().is_none()); } -} +} \ No newline at end of file From bc042eeab9345c000012bea2c9941b3bd1d45b71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:36:11 -0700 Subject: [PATCH 074/118] test(core): format BiDi WebSocket endpoint regressions --- .../originweave-core/tests/webdriver_bidi_websocket_endpoint.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index 3549d293b..ecf5df6b6 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -186,4 +186,4 @@ fn endpoint_error_contract_is_deterministic_and_source_free() { assert!(!error.to_string().is_empty()); assert!(error.source().is_none()); } -} \ No newline at end of file +} From 5d7906a8bfc17ff2abbbb499d9f3be4ef77f297e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:11:54 -0700 Subject: [PATCH 075/118] test(core): require exact BiDi endpoint session correlation --- ...iver_bidi_websocket_session_correlation.rs | 72 +++++++++++++++++++ 1 file changed, 72 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs new file mode 100644 index 000000000..7945cf629 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs @@ -0,0 +1,72 @@ +use std::error::Error; + +use originweave_core::{ + WebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketEndpointCorrelationError, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const OTHER_SESSION_ID: &str = "11234567-89ab-cdef-0123-456789abcdef"; + +fn endpoint() -> WebDriverBiDiWebSocketEndpoint { + let result = WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://127.0.0.1:9515/session/{SESSION_ID}" + )); + assert!(result.is_ok(), "{result:?}"); + let Ok(endpoint) = result else { + unreachable!("asserted valid endpoint") + }; + endpoint +} + +#[test] +fn exact_session_identity_correlation_preserves_bounded_endpoint_metadata() { + let result = endpoint().correlate_session_id(SESSION_ID); + assert!(result.is_ok(), "{result:?}"); + let Ok(correlated) = result else { + return; + }; + + assert_eq!( + correlated.as_str(), + format!("ws://127.0.0.1:9515/session/{SESSION_ID}") + ); + assert!(!correlated.is_secure()); + assert_eq!(correlated.host(), "127.0.0.1"); + assert_eq!(correlated.port(), 9515); + assert_eq!(correlated.session_id(), SESSION_ID); +} + +#[test] +fn a_different_canonical_session_identity_fails_closed() { + assert!(matches!( + endpoint().correlate_session_id(OTHER_SESSION_ID), + Err(WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch) + )); +} + +#[test] +fn malformed_expected_session_identity_is_rejected_before_comparison() { + for expected in [ + "", + "01234567-89ab-cdef-0123-456789abcdeF", + "0123456789ab-cdef-0123-456789abcdef", + "01234567-89ab-cdef-0123-456789abcdeg", + "01234567_89ab-cdef-0123-456789abcdef", + ] { + assert!(matches!( + endpoint().correlate_session_id(expected), + Err(WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId) + )); + } +} + +#[test] +fn session_correlation_errors_are_deterministic_and_source_free() { + for error in [ + WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId, + WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch, + ] { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_none()); + } +} From 96fdd9dc9eeb54a03b9392514aa4304811ed36ea Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:16:29 -0700 Subject: [PATCH 076/118] style(core): apply canonical BiDi session-correlation formatting --- .../tests/webdriver_bidi_websocket_session_correlation.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs index 7945cf629..074faf57e 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_session_correlation.rs @@ -8,9 +8,8 @@ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const OTHER_SESSION_ID: &str = "11234567-89ab-cdef-0123-456789abcdef"; fn endpoint() -> WebDriverBiDiWebSocketEndpoint { - let result = WebDriverBiDiWebSocketEndpoint::new(&format!( - "ws://127.0.0.1:9515/session/{SESSION_ID}" - )); + let result = + WebDriverBiDiWebSocketEndpoint::new(&format!("ws://127.0.0.1:9515/session/{SESSION_ID}")); assert!(result.is_ok(), "{result:?}"); let Ok(endpoint) = result else { unreachable!("asserted valid endpoint") From c4d2e4d77d6bab17dfec6b0353f1bf8c2b28f86c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:20:20 -0700 Subject: [PATCH 077/118] feat(core): correlate BiDi endpoint with exact session --- .../src/webdriver_bidi_websocket_endpoint.rs | 92 +++++++++++++++++++ 1 file changed, 92 insertions(+) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 3a555493b..6b8955a58 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -23,6 +23,17 @@ pub struct WebDriverBiDiWebSocketEndpoint { session_id: String, } +/// One admitted WebDriver BiDi WebSocket endpoint correlated to an expected session id. +/// +/// Correlation proves only that the endpoint resource and the caller-supplied expected session id +/// contain the same canonical UUID text. The expected session id must itself come from a trusted +/// session-creation boundary. This value does not authenticate Chromium, ChromeDriver, the caller, +/// the operating-system peer, TLS, policy, or Agent authority, and it does not establish a socket. +#[derive(Debug, PartialEq, Eq)] +pub struct CorrelatedWebDriverBiDiWebSocketEndpoint { + endpoint: WebDriverBiDiWebSocketEndpoint, +} + impl WebDriverBiDiWebSocketEndpoint { /// Admit one bounded canonical first-fixture WebDriver BiDi endpoint. pub fn new(value: &str) -> Result { @@ -128,6 +139,30 @@ impl WebDriverBiDiWebSocketEndpoint { }) } + /// Correlate this endpoint resource to one exact expected WebDriver session id. + /// + /// The endpoint is consumed so downstream connection code can require the correlated type and + /// cannot accidentally retain an uncorrelated copy. This comparison does not establish that the + /// caller-supplied expected id is authentic; the caller must obtain that id from its trusted + /// session-creation boundary. + pub fn correlate_session_id( + self, + expected_session_id: &str, + ) -> Result< + CorrelatedWebDriverBiDiWebSocketEndpoint, + WebDriverBiDiWebSocketEndpointCorrelationError, + > { + if !is_canonical_session_uuid(expected_session_id) { + return Err( + WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId, + ); + } + if self.session_id != expected_session_id { + return Err(WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch); + } + Ok(CorrelatedWebDriverBiDiWebSocketEndpoint { endpoint: self }) + } + /// Return the exact admitted endpoint text. #[must_use] pub fn as_str(&self) -> &str { @@ -159,6 +194,38 @@ impl WebDriverBiDiWebSocketEndpoint { } } +impl CorrelatedWebDriverBiDiWebSocketEndpoint { + /// Return the exact admitted endpoint text. + #[must_use] + pub fn as_str(&self) -> &str { + self.endpoint.as_str() + } + + /// Return whether the endpoint uses `wss` rather than `ws`. + #[must_use] + pub const fn is_secure(&self) -> bool { + self.endpoint.is_secure() + } + + /// Return the canonical loopback listener host without IPv6 brackets. + #[must_use] + pub fn host(&self) -> &str { + self.endpoint.host() + } + + /// Return the explicit nonzero listener port. + #[must_use] + pub const fn port(&self) -> u16 { + self.endpoint.port() + } + + /// Return the exact session id proven equal to the caller-supplied expected session id. + #[must_use] + pub fn session_id(&self) -> &str { + self.endpoint.session_id() + } +} + fn is_canonical_session_uuid(value: &str) -> bool { let bytes = value.as_bytes(); if bytes.len() != 36 { @@ -227,3 +294,28 @@ impl fmt::Display for WebDriverBiDiWebSocketEndpointAdmissionError { } impl std::error::Error for WebDriverBiDiWebSocketEndpointAdmissionError {} + +/// Fail-closed errors while correlating an admitted endpoint with an expected session id. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiWebSocketEndpointCorrelationError { + /// The expected session id is not one canonical lower-case UUID representation. + InvalidExpectedSessionId, + /// The endpoint resource belongs to a different canonical session id. + SessionIdMismatch, +} + +impl fmt::Display for WebDriverBiDiWebSocketEndpointCorrelationError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let message = match self { + Self::InvalidExpectedSessionId => { + "expected WebDriver session id is not a canonical lower-case UUID" + } + Self::SessionIdMismatch => { + "WebDriver BiDi WebSocket endpoint session id does not match the expected session" + } + }; + f.write_str(message) + } +} + +impl std::error::Error for WebDriverBiDiWebSocketEndpointCorrelationError {} From 45935f51eadbce5d64fc480abbc3165cc7e373cc Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:21:22 -0700 Subject: [PATCH 078/118] feat(core): export correlated BiDi endpoint contract --- crates/originweave-core/src/lib.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 67753ed6c..27929c727 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -89,6 +89,7 @@ pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; pub use webdriver_bidi_websocket_endpoint::{ - MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, WebDriverBiDiWebSocketEndpoint, - WebDriverBiDiWebSocketEndpointAdmissionError, + CorrelatedWebDriverBiDiWebSocketEndpoint, MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, + WebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketEndpointAdmissionError, + WebDriverBiDiWebSocketEndpointCorrelationError, }; From 70461f197bbef8fc9f81985d2f4b9e80619a4b2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:24:58 -0700 Subject: [PATCH 079/118] style(core): apply canonical session-correlation formatting --- .../originweave-core/src/webdriver_bidi_websocket_endpoint.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 6b8955a58..c95c309e1 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -153,9 +153,7 @@ impl WebDriverBiDiWebSocketEndpoint { WebDriverBiDiWebSocketEndpointCorrelationError, > { if !is_canonical_session_uuid(expected_session_id) { - return Err( - WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId, - ); + return Err(WebDriverBiDiWebSocketEndpointCorrelationError::InvalidExpectedSessionId); } if self.session_id != expected_session_id { return Err(WebDriverBiDiWebSocketEndpointCorrelationError::SessionIdMismatch); From 4047cef971bfdc43e352fca7b8d98c3efee31f41 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 23:29:43 -0700 Subject: [PATCH 080/118] docs(changelog): record BiDi session correlation --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index f87011269..3be520bcd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Typed WebDriver BiDi response-envelope correlation that preserves success/error classification, rejects success responses with absent ids, treats nullable error ids as explicitly uncorrelatable, and prevents correlated error envelopes from becoming success correlation evidence without raw JSON parsing or browser/Agent authority grants. - Bounded raw WebDriver BiDi response-document admission before JSON parsing, with a 65,536-byte product safety budget, exact wire-text retention, JSON-whitespace-aware top-level object-boundary checks, and typed fail-closed errors; this coarse boundary deliberately does not claim JSON validity, response correlation, browser authenticity, or Agent authority. - One consuming bounded WebDriver BiDi response-document correlation boundary that parses the exact admitted JSON document and feeds only its typed response kind and protocol-range id into the existing exact command-correlation gate, preserving nested parser/correlation error sources without authenticating transport or granting browser/Agent authority. +- Consuming WebDriver BiDi WebSocket endpoint/session correlation that validates one caller-supplied canonical session UUID and rejects exact session mismatches before later transport use; the correlated type preserves only bounded endpoint metadata and does not authenticate Chromium, ChromeDriver, the caller, or the socket peer. - Fail-closed WebDriver BiDi `script.NodeRemoteValue` admission that requires the exact remote type `node` and a non-empty `sharedId` within the same UTF-8 identifier budget as browser session and context identifiers, rejecting control and whitespace so an untrusted `locateNodes` item cannot be retained as a later typed-input handle without a usable shared node identity. - Same-call `locateNodes` result admission that revalidates the exact current session, browsing context, canonical origin, and document epoch, rejects an over-budget or non-node result, and translates each admitted `sharedId` through the authority registry into an `ObservedNodeHandle` without performing browser I/O. - Same-call QueryNodes admission that transfers a non-cloneable SemanticObservation protocol-use proof by ownership into `bind_current_nodes` before an untrusted `locateNodes` result can become current `ObservedNodeHandle` values, so Navigation-only or TypedInput-only proofs cannot mint observation handles. From 48ad304eb4c2a18848d5fc4fbf6e2969f5576bfb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:10:04 -0700 Subject: [PATCH 081/118] test(core): require explicit BiDi connect target --- ...webdriver_bidi_websocket_connect_target.rs | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs new file mode 100644 index 000000000..b71957f9d --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs @@ -0,0 +1,69 @@ +use std::{error::Error, net::SocketAddr}; + +use originweave_core::{ + CorrelatedWebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketConnectTargetError, + WebDriverBiDiWebSocketEndpoint, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +fn correlated(endpoint: &str) -> CorrelatedWebDriverBiDiWebSocketEndpoint { + let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); + assert!(admitted.is_ok(), "{admitted:?}"); + let Ok(admitted) = admitted else { + unreachable!("asserted valid endpoint") + }; + let correlated = admitted.correlate_session_id(SESSION_ID); + assert!(correlated.is_ok(), "{correlated:?}"); + let Ok(correlated) = correlated else { + unreachable!("asserted correlated endpoint") + }; + correlated +} + +#[test] +fn explicit_ipv4_loopback_becomes_exact_no_dns_connect_target() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); + assert!(result.is_ok(), "{result:?}"); + let Ok(target) = result else { + return; + }; + + assert_eq!(target.socket_addr(), SocketAddr::from(([127, 0, 0, 1], 9515))); + assert!(!target.requires_tls()); + assert_eq!(target.session_id(), SESSION_ID); +} + +#[test] +fn explicit_ipv6_loopback_preserves_exact_destination_and_tls_requirement() { + let endpoint = format!("wss://[::1]:9443/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); + assert!(result.is_ok(), "{result:?}"); + let Ok(target) = result else { + return; + }; + + assert_eq!(target.socket_addr(), SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 9443))); + assert!(target.requires_tls()); + assert_eq!(target.session_id(), SESSION_ID); +} + +#[test] +fn localhost_name_never_silently_inherits_ambient_dns_authority() { + let endpoint = format!("ws://localhost:9515/session/{SESSION_ID}"); + assert!(matches!( + correlated(&endpoint).into_explicit_connect_target(), + Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired) + )); +} + +#[test] +fn connect_target_errors_are_deterministic_and_source_free() { + let error = WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired; + assert_eq!( + error.to_string(), + "WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution" + ); + assert!(error.source().is_none()); +} From 3cc67111a08c5f5ae1c748bab795273a6cad4c89 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:16:43 -0700 Subject: [PATCH 082/118] test(core): format explicit BiDi connect target regressions --- .../tests/webdriver_bidi_websocket_connect_target.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs index b71957f9d..d068b3d5a 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs @@ -30,7 +30,10 @@ fn explicit_ipv4_loopback_becomes_exact_no_dns_connect_target() { return; }; - assert_eq!(target.socket_addr(), SocketAddr::from(([127, 0, 0, 1], 9515))); + assert_eq!( + target.socket_addr(), + SocketAddr::from(([127, 0, 0, 1], 9515)) + ); assert!(!target.requires_tls()); assert_eq!(target.session_id(), SESSION_ID); } @@ -44,7 +47,10 @@ fn explicit_ipv6_loopback_preserves_exact_destination_and_tls_requirement() { return; }; - assert_eq!(target.socket_addr(), SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 9443))); + assert_eq!( + target.socket_addr(), + SocketAddr::from(([0, 0, 0, 0, 0, 0, 0, 1], 9443)) + ); assert!(target.requires_tls()); assert_eq!(target.session_id(), SESSION_ID); } From 2e9898dea986a54f5c4cbd0adfaf8b96adae8ded Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:17:50 -0700 Subject: [PATCH 083/118] feat(core): derive explicit BiDi loopback connect targets --- ...webdriver_bidi_websocket_connect_target.rs | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs new file mode 100644 index 000000000..de4bf5a69 --- /dev/null +++ b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs @@ -0,0 +1,89 @@ +//! Explicit no-DNS connection targets for correlated WebDriver BiDi endpoints. +//! +//! This boundary converts only literal loopback listener identities into exact socket metadata. +//! It deliberately refuses `localhost` so a later connector cannot silently inherit ambient DNS +//! authority from an admitted WebDriver endpoint. The resulting value does not open a socket, +//! authenticate a peer, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. + +use std::{fmt, net::{Ipv4Addr, Ipv6Addr, SocketAddr}}; + +use crate::CorrelatedWebDriverBiDiWebSocketEndpoint; + +/// An exact loopback socket destination derived from one correlated WebDriver BiDi endpoint. +/// +/// The destination is inert connection metadata. It proves only that the already-admitted endpoint +/// named a literal loopback IP address, retained an explicit nonzero port, and was correlated to the +/// expected WebDriver session id. A runtime connector must independently enforce peer identity, +/// TLS, WebSocket, process, policy, and browser authority before using transport I/O. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct WebDriverBiDiWebSocketConnectTarget { + socket_addr: SocketAddr, + requires_tls: bool, + session_id: String, +} + +impl WebDriverBiDiWebSocketConnectTarget { + /// Return the exact loopback socket destination without performing name resolution. + #[must_use] + pub const fn socket_addr(&self) -> SocketAddr { + self.socket_addr + } + + /// Return whether the admitted endpoint requires a TLS-protected WebSocket transport. + #[must_use] + pub const fn requires_tls(&self) -> bool { + self.requires_tls + } + + /// Return the exact WebDriver session id established by the preceding correlation boundary. + #[must_use] + pub fn session_id(&self) -> &str { + &self.session_id + } +} + +impl CorrelatedWebDriverBiDiWebSocketEndpoint { + /// Consume this correlated endpoint and derive one exact no-DNS loopback socket destination. + /// + /// Literal IPv4 and IPv6 loopback hosts become an exact [`SocketAddr`]. Any admitted host that + /// is not an IP literal—including `localhost`—fails closed so the caller must perform an + /// explicit, separately trusted name-resolution step rather than inheriting ambient resolver + /// authority. This method performs no DNS lookup, socket I/O, peer authentication, TLS, or + /// WebSocket handshake. + pub fn into_explicit_connect_target( + self, + ) -> Result { + let socket_addr = if let Ok(ipv4) = self.host().parse::() { + SocketAddr::from((ipv4, self.port())) + } else if let Ok(ipv6) = self.host().parse::() { + SocketAddr::from((ipv6, self.port())) + } else { + return Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired); + }; + + Ok(WebDriverBiDiWebSocketConnectTarget { + socket_addr, + requires_tls: self.is_secure(), + session_id: self.session_id().to_owned(), + }) + } +} + +/// Fail-closed errors while deriving an explicit WebDriver BiDi socket destination. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiWebSocketConnectTargetError { + /// The admitted endpoint used a host name and therefore requires explicit trusted resolution. + NameResolutionRequired, +} + +impl fmt::Display for WebDriverBiDiWebSocketConnectTargetError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::NameResolutionRequired => formatter.write_str( + "WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution", + ), + } + } +} + +impl std::error::Error for WebDriverBiDiWebSocketConnectTargetError {} From aace3a8587ed7d295ff6f5dd240c3d0995f3a10f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:18:15 -0700 Subject: [PATCH 084/118] feat(core): export explicit BiDi connect target contract --- crates/originweave-core/src/lib.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 27929c727..eaeecd064 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -37,6 +37,7 @@ mod webdriver_bidi_response_document; mod webdriver_bidi_response_document_correlation; mod webdriver_bidi_response_envelope; mod webdriver_bidi_result; +mod webdriver_bidi_websocket_connect_target; mod webdriver_bidi_websocket_endpoint; pub use browser_authority_registry::BrowserAuthorityRegistry; @@ -88,6 +89,9 @@ pub use webdriver_bidi_response_envelope::{ pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; +pub use webdriver_bidi_websocket_connect_target::{ + WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketConnectTargetError, +}; pub use webdriver_bidi_websocket_endpoint::{ CorrelatedWebDriverBiDiWebSocketEndpoint, MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES, WebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketEndpointAdmissionError, From 2e43ad65d57fd32a6f4a78c386e19b507e8d10a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:18:46 -0700 Subject: [PATCH 085/118] style(core): format explicit BiDi connect target --- .../src/webdriver_bidi_websocket_connect_target.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs index de4bf5a69..26e7998b7 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs @@ -5,7 +5,10 @@ //! authority from an admitted WebDriver endpoint. The resulting value does not open a socket, //! authenticate a peer, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. -use std::{fmt, net::{Ipv4Addr, Ipv6Addr, SocketAddr}}; +use std::{ + fmt, + net::{Ipv4Addr, Ipv6Addr, SocketAddr}, +}; use crate::CorrelatedWebDriverBiDiWebSocketEndpoint; From 6a21bedff31217372f63e0ef1a2f5b343fee749d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:04:05 -0700 Subject: [PATCH 086/118] test(core): require BiDi connect target changelog evidence --- ...ebdriver_bidi_connect_target_governance.py | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 tests/test_webdriver_bidi_connect_target_governance.py diff --git a/tests/test_webdriver_bidi_connect_target_governance.py b/tests/test_webdriver_bidi_connect_target_governance.py new file mode 100644 index 000000000..b593727ff --- /dev/null +++ b/tests/test_webdriver_bidi_connect_target_governance.py @@ -0,0 +1,27 @@ +"""Governance regression for explicit WebDriver BiDi socket destinations.""" + +from pathlib import Path +import unittest + + +ROOT = Path(__file__).resolve().parents[1] +CHANGELOG = ROOT / "CHANGELOG.md" + + +class WebDriverBiDiConnectTargetGovernanceTests(unittest.TestCase): + """Keep the active no-DNS transport boundary visible in release evidence.""" + + def test_changelog_records_explicit_no_dns_connect_target_boundary(self) -> None: + """The production connect-target slice must have a truthful Unreleased record.""" + changelog = CHANGELOG.read_text(encoding="utf-8") + self.assertIn( + "Explicit no-DNS WebDriver BiDi loopback connection targets", + changelog, + ) + self.assertIn("localhost", changelog) + self.assertIn("no socket I/O", changelog) + self.assertIn("no Agent authority", changelog) + + +if __name__ == "__main__": + unittest.main() From 7e303600a03e7d24a5d6df3237cd63f280ca9a23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 01:12:50 -0700 Subject: [PATCH 087/118] docs(core): record explicit BiDi connect target boundary --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3be520bcd..64946b209 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Explicit no-DNS WebDriver BiDi loopback connection targets that derive exact IPv4/IPv6 loopback `SocketAddr` metadata from a session-correlated endpoint, reject `localhost` as requiring separately trusted name resolution, preserve the TLS requirement and exact session id, perform no socket I/O, and grant no Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Versioned browser-protocol adapter metadata that distinguishes WebDriver BiDi from pinned CDP, binds bounded adapter/browser revision tokens to an explicit duplicate-free capability set, normalizes capability-set identity independently of caller ordering, and exposes typed fail-closed capability requirements without granting browser, action, network, or secret authority by protocol kind alone. - Canonical OriginWeave protocol-version parsing for exact `originweave/.` syntax, with typed fail-closed rejection of malformed, ambiguous, overflowed, or noncanonical serialized generations; parsing does not negotiate compatibility or grant adapter authority. From dc5cfd8a55f1dee5677eedebb3e93585003d7b83 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 02:07:52 -0700 Subject: [PATCH 088/118] test(core): preserve BiDi endpoint across resolver handoff --- ...webdriver_bidi_websocket_connect_target.rs | 30 +++++++++++++++++-- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs index d068b3d5a..85f26f658 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_connect_target.rs @@ -58,15 +58,39 @@ fn explicit_ipv6_loopback_preserves_exact_destination_and_tls_requirement() { #[test] fn localhost_name_never_silently_inherits_ambient_dns_authority() { let endpoint = format!("ws://localhost:9515/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); assert!(matches!( - correlated(&endpoint).into_explicit_connect_target(), - Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired) + &result, + Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired { .. }) )); } +#[test] +fn name_resolution_failure_preserves_correlated_endpoint_for_trusted_resolver() { + let endpoint = format!("ws://localhost:9515/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); + let Err(error) = result else { + return; + }; + + assert_eq!(error.correlated_endpoint().as_str(), endpoint); + assert_eq!(error.correlated_endpoint().session_id(), SESSION_ID); + assert!(!error.correlated_endpoint().is_secure()); + assert_eq!(error.correlated_endpoint().port(), 9515); + + let recovered = error.into_correlated_endpoint(); + assert_eq!(recovered.as_str(), endpoint); + assert_eq!(recovered.session_id(), SESSION_ID); +} + #[test] fn connect_target_errors_are_deterministic_and_source_free() { - let error = WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired; + let endpoint = format!("ws://localhost:9515/session/{SESSION_ID}"); + let result = correlated(&endpoint).into_explicit_connect_target(); + let Err(error) = result else { + return; + }; + assert_eq!( error.to_string(), "WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution" From 3a9506c5b14113ff3fe86a84965876a549827759 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 02:09:36 -0700 Subject: [PATCH 089/118] fix(core): retain correlated BiDi endpoint on resolver handoff --- ...webdriver_bidi_websocket_connect_target.rs | 49 ++++++++++++++++--- 1 file changed, 41 insertions(+), 8 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs index 26e7998b7..085335f45 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs @@ -2,8 +2,10 @@ //! //! This boundary converts only literal loopback listener identities into exact socket metadata. //! It deliberately refuses `localhost` so a later connector cannot silently inherit ambient DNS -//! authority from an admitted WebDriver endpoint. The resulting value does not open a socket, -//! authenticate a peer, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. +//! authority from an admitted WebDriver endpoint. When explicit trusted name resolution is needed, +//! the typed error preserves the correlated endpoint instead of discarding its session evidence. +//! The resulting value does not open a socket, authenticate a peer, negotiate TLS, perform a +//! WebSocket handshake, or grant Agent authority. use std::{ fmt, @@ -51,8 +53,10 @@ impl CorrelatedWebDriverBiDiWebSocketEndpoint { /// Literal IPv4 and IPv6 loopback hosts become an exact [`SocketAddr`]. Any admitted host that /// is not an IP literal—including `localhost`—fails closed so the caller must perform an /// explicit, separately trusted name-resolution step rather than inheriting ambient resolver - /// authority. This method performs no DNS lookup, socket I/O, peer authentication, TLS, or - /// WebSocket handshake. + /// authority. The name-resolution-required error retains this correlated endpoint so that + /// trusted resolver handoff does not require reconstructing or recorrelation of session evidence. + /// This method performs no DNS lookup, socket I/O, peer authentication, TLS, or WebSocket + /// handshake. pub fn into_explicit_connect_target( self, ) -> Result { @@ -61,7 +65,11 @@ impl CorrelatedWebDriverBiDiWebSocketEndpoint { } else if let Ok(ipv6) = self.host().parse::() { SocketAddr::from((ipv6, self.port())) } else { - return Err(WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired); + return Err( + WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired { + correlated_endpoint: self, + }, + ); }; Ok(WebDriverBiDiWebSocketConnectTarget { @@ -73,16 +81,41 @@ impl CorrelatedWebDriverBiDiWebSocketEndpoint { } /// Fail-closed errors while deriving an explicit WebDriver BiDi socket destination. -#[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Debug, PartialEq, Eq)] pub enum WebDriverBiDiWebSocketConnectTargetError { /// The admitted endpoint used a host name and therefore requires explicit trusted resolution. - NameResolutionRequired, + NameResolutionRequired { + /// The still-correlated endpoint that must be handed to a separately trusted resolver. + correlated_endpoint: CorrelatedWebDriverBiDiWebSocketEndpoint, + }, +} + +impl WebDriverBiDiWebSocketConnectTargetError { + /// Borrow the correlated endpoint preserved for an explicit trusted resolver handoff. + #[must_use] + pub const fn correlated_endpoint(&self) -> &CorrelatedWebDriverBiDiWebSocketEndpoint { + match self { + Self::NameResolutionRequired { + correlated_endpoint, + } => correlated_endpoint, + } + } + + /// Recover the correlated endpoint for an explicit trusted resolver handoff. + #[must_use] + pub fn into_correlated_endpoint(self) -> CorrelatedWebDriverBiDiWebSocketEndpoint { + match self { + Self::NameResolutionRequired { + correlated_endpoint, + } => correlated_endpoint, + } + } } impl fmt::Display for WebDriverBiDiWebSocketConnectTargetError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::NameResolutionRequired => formatter.write_str( + Self::NameResolutionRequired { .. } => formatter.write_str( "WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution", ), } From d34c528d5cbc06403c36feea97147b4f0cf2d262 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:44:39 -0700 Subject: [PATCH 090/118] test(core): require exact BiDi socket peer verification --- ...webdriver_bidi_socket_peer_verification.rs | 107 ++++++++++++++++++ 1 file changed, 107 insertions(+) create mode 100644 crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs diff --git a/crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs b/crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs new file mode 100644 index 000000000..dd20a4ef1 --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_socket_peer_verification.rs @@ -0,0 +1,107 @@ +use std::{error::Error, net::SocketAddr}; + +use originweave_core::{ + CorrelatedWebDriverBiDiWebSocketEndpoint, WebDriverBiDiSocketPeerVerificationError, + WebDriverBiDiWebSocketEndpoint, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +fn connect_target(endpoint: &str) -> originweave_core::WebDriverBiDiWebSocketConnectTarget { + let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); + assert!(admitted.is_ok(), "{admitted:?}"); + let Ok(admitted) = admitted else { + unreachable!("asserted valid endpoint") + }; + + let correlated: Result = + admitted.correlate_session_id(SESSION_ID); + assert!(correlated.is_ok(), "{correlated:?}"); + let Ok(correlated) = correlated else { + unreachable!("asserted correlated endpoint") + }; + + let target = correlated.into_explicit_connect_target(); + assert!(target.is_ok(), "{target:?}"); + let Ok(target) = target else { + unreachable!("asserted literal loopback target") + }; + target +} + +#[test] +fn exact_connected_peer_becomes_verified_transport_metadata() { + let endpoint = format!("wss://127.0.0.1:9443/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let peer = SocketAddr::from(([127, 0, 0, 1], 9443)); + + let verified = target.verify_connected_peer(peer); + assert!(verified.is_ok(), "{verified:?}"); + let Ok(verified) = verified else { + return; + }; + + assert_eq!(verified.socket_addr(), peer); + assert!(verified.requires_tls()); + assert_eq!(verified.session_id(), SESSION_ID); +} + +#[test] +fn connected_peer_with_wrong_port_fails_closed() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([127, 0, 0, 1], 9516)); + + let result = target.verify_connected_peer(actual); + assert_eq!( + result, + Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { + expected: SocketAddr::from(([127, 0, 0, 1], 9515)), + actual, + }) + ); +} + +#[test] +fn connected_peer_with_different_address_fails_closed() { + let endpoint = format!("ws://[::1]:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([127, 0, 0, 1], 9515)); + + let result = target.verify_connected_peer(actual); + assert!(matches!( + result, + Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { .. }) + )); +} + +#[test] +fn non_loopback_observed_peer_cannot_inherit_approved_loopback_authority() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([192, 0, 2, 10], 9515)); + + let result = target.verify_connected_peer(actual); + assert!(matches!( + result, + Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { .. }) + )); +} + +#[test] +fn peer_mismatch_error_is_deterministic_and_source_free() { + let endpoint = format!("ws://127.0.0.1:9515/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let actual = SocketAddr::from(([127, 0, 0, 1], 9516)); + + let result = target.verify_connected_peer(actual); + let Err(error) = result else { + return; + }; + + assert_eq!( + error.to_string(), + "connected WebDriver BiDi socket peer does not match the approved destination" + ); + assert!(error.source().is_none()); +} From d7cf226ba92f404262d4bab02c3fa622e676f459 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:49:13 -0700 Subject: [PATCH 091/118] feat(core): verify exact BiDi socket peer --- ...webdriver_bidi_websocket_connect_target.rs | 89 ++++++++++++++++++- 1 file changed, 85 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs index 085335f45..5002731db 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs @@ -4,8 +4,9 @@ //! It deliberately refuses `localhost` so a later connector cannot silently inherit ambient DNS //! authority from an admitted WebDriver endpoint. When explicit trusted name resolution is needed, //! the typed error preserves the correlated endpoint instead of discarding its session evidence. -//! The resulting value does not open a socket, authenticate a peer, negotiate TLS, perform a -//! WebSocket handshake, or grant Agent authority. +//! A separately observed connected peer must also match the approved socket destination exactly +//! before it becomes verified transport metadata. These values do not open a socket, authenticate +//! a process, negotiate TLS, perform a WebSocket handshake, or grant Agent authority. use std::{ fmt, @@ -18,8 +19,9 @@ use crate::CorrelatedWebDriverBiDiWebSocketEndpoint; /// /// The destination is inert connection metadata. It proves only that the already-admitted endpoint /// named a literal loopback IP address, retained an explicit nonzero port, and was correlated to the -/// expected WebDriver session id. A runtime connector must independently enforce peer identity, -/// TLS, WebSocket, process, policy, and browser authority before using transport I/O. +/// expected WebDriver session id. A runtime connector must independently establish a connection and +/// verify its observed peer before treating that transport as the approved destination. TLS, +/// WebSocket, process, policy, and browser authority remain separate boundaries. #[derive(Debug, Clone, PartialEq, Eq)] pub struct WebDriverBiDiWebSocketConnectTarget { socket_addr: SocketAddr, @@ -45,8 +47,87 @@ impl WebDriverBiDiWebSocketConnectTarget { pub fn session_id(&self) -> &str { &self.session_id } + + /// Consume this approved destination and verify one observed connected socket peer exactly. + /// + /// Matching requires the complete [`SocketAddr`]—IP address and port—to equal the approved + /// no-DNS destination. A mismatch consumes the target and fails closed, preventing a connector + /// from accidentally reusing the same authority after observing a different peer. Success + /// produces inert verified-peer metadata only; it does not authenticate an OS process, + /// negotiate TLS, perform a WebSocket handshake, or grant browser/Agent authority. + pub fn verify_connected_peer( + self, + observed_peer: SocketAddr, + ) -> Result { + let expected = self.socket_addr; + if observed_peer != expected { + return Err(WebDriverBiDiSocketPeerVerificationError::PeerMismatch { + expected, + actual: observed_peer, + }); + } + + Ok(VerifiedWebDriverBiDiSocketPeer { + connect_target: self, + }) + } +} + +/// Inert metadata proving that a connected peer exactly matched the approved BiDi destination. +/// +/// This value carries only the destination, TLS requirement, and correlated WebDriver session id +/// already established by preceding boundaries. It does not prove process identity, TLS peer +/// identity, WebSocket protocol state, browser authenticity, policy authorization, or Agent action +/// authority. +#[derive(Debug, PartialEq, Eq)] +pub struct VerifiedWebDriverBiDiSocketPeer { + connect_target: WebDriverBiDiWebSocketConnectTarget, +} + +impl VerifiedWebDriverBiDiSocketPeer { + /// Return the exact approved and observed socket peer address. + #[must_use] + pub const fn socket_addr(&self) -> SocketAddr { + self.connect_target.socket_addr() + } + + /// Return whether the correlated endpoint still requires TLS before WebSocket use. + #[must_use] + pub const fn requires_tls(&self) -> bool { + self.connect_target.requires_tls() + } + + /// Return the exact correlated WebDriver session id. + #[must_use] + pub fn session_id(&self) -> &str { + self.connect_target.session_id() + } +} + +/// Fail-closed errors while verifying an observed BiDi socket peer. +#[derive(Debug, PartialEq, Eq)] +pub enum WebDriverBiDiSocketPeerVerificationError { + /// The connected peer differed from the exact destination approved before connection. + PeerMismatch { + /// Exact socket address that the connector was authorized to reach. + expected: SocketAddr, + /// Socket peer address observed after connection. + actual: SocketAddr, + }, } +impl fmt::Display for WebDriverBiDiSocketPeerVerificationError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::PeerMismatch { .. } => formatter.write_str( + "connected WebDriver BiDi socket peer does not match the approved destination", + ), + } + } +} + +impl std::error::Error for WebDriverBiDiSocketPeerVerificationError {} + impl CorrelatedWebDriverBiDiWebSocketEndpoint { /// Consume this correlated endpoint and derive one exact no-DNS loopback socket destination. /// From 89fc9c3d5524c2bd76ef4cb4c27398acf5146b32 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 03:49:45 -0700 Subject: [PATCH 092/118] feat(core): export verified BiDi peer contract --- crates/originweave-core/src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index eaeecd064..b9d69c8b0 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -90,6 +90,7 @@ pub use webdriver_bidi_result::{ ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError, }; pub use webdriver_bidi_websocket_connect_target::{ + VerifiedWebDriverBiDiSocketPeer, WebDriverBiDiSocketPeerVerificationError, WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketConnectTargetError, }; pub use webdriver_bidi_websocket_endpoint::{ From f62d20496b1d2ec4aa257ef28944b2edc2d57ff2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:03:34 -0700 Subject: [PATCH 093/118] test(core): require BiDi socket-peer release evidence --- tests/test_webdriver_bidi_connect_target_governance.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_webdriver_bidi_connect_target_governance.py b/tests/test_webdriver_bidi_connect_target_governance.py index b593727ff..de7949478 100644 --- a/tests/test_webdriver_bidi_connect_target_governance.py +++ b/tests/test_webdriver_bidi_connect_target_governance.py @@ -22,6 +22,14 @@ def test_changelog_records_explicit_no_dns_connect_target_boundary(self) -> None self.assertIn("no socket I/O", changelog) self.assertIn("no Agent authority", changelog) + def test_changelog_records_exact_connected_peer_verification_boundary(self) -> None: + """Verified socket-peer metadata must be visible without overstating transport trust.""" + changelog = CHANGELOG.read_text(encoding="utf-8") + self.assertIn("Exact WebDriver BiDi socket-peer verification", changelog) + self.assertIn("IP address and port", changelog) + self.assertIn("does not authenticate an OS process", changelog) + self.assertIn("does not negotiate TLS", changelog) + if __name__ == "__main__": unittest.main() From d52cdfefb8336df4edf4fb49d57ca99e1a6947e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:06:09 -0700 Subject: [PATCH 094/118] docs(changelog): record BiDi socket-peer verification --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 64946b209..b8a348bce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Exact WebDriver BiDi socket-peer verification that consumes an approved no-DNS connect target, requires the observed IP address and port to match exactly, preserves the TLS requirement and exact correlated session id, and remains inert metadata that does not authenticate an OS process, does not negotiate TLS, perform a WebSocket handshake, or grant Agent authority. - Explicit no-DNS WebDriver BiDi loopback connection targets that derive exact IPv4/IPv6 loopback `SocketAddr` metadata from a session-correlated endpoint, reject `localhost` as requiring separately trusted name resolution, preserve the TLS requirement and exact session id, perform no socket I/O, and grant no Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - Versioned browser-protocol adapter metadata that distinguishes WebDriver BiDi from pinned CDP, binds bounded adapter/browser revision tokens to an explicit duplicate-free capability set, normalizes capability-set identity independently of caller ordering, and exposes typed fail-closed capability requirements without granting browser, action, network, or secret authority by protocol kind alone. @@ -77,7 +78,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. - Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target cycle state, and hop capacity before policy state changes. -- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. +- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. - Direct connection code accepts only an explicit `SocketAddr`, never a hostname, and does not read proxy environment variables. - Established streams are discarded when peer inspection fails or the observed remote IP or port differs from the approved socket. - TLS accepts only an already verified direct stream, never a hostname or new socket, and requires the TLS origin to match the transport-authority origin exactly. From fa510a0e180b08f38f0e92920b16b874815504c4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:08:26 -0700 Subject: [PATCH 095/118] fix(changelog): preserve direct TCP release wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b8a348bce..7ca383831 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -78,7 +78,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. - Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target cycle state, and hop capacity before policy state changes. -- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. +- Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. - Direct connection code accepts only an explicit `SocketAddr`, never a hostname, and does not read proxy environment variables. - Established streams are discarded when peer inspection fails or the observed remote IP or port differs from the approved socket. - TLS accepts only an already verified direct stream, never a hostname or new socket, and requires the TLS origin to match the transport-authority origin exactly. From fcde9755057a38827c0efc4d1fbbf3a6d6b9608a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:30:54 -0700 Subject: [PATCH 096/118] test(network): require bounded BiDi loopback TCP connection --- .../tests/webdriver_bidi_tcp_connection.rs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs b/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs new file mode 100644 index 000000000..5cfd49783 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs @@ -0,0 +1,100 @@ +use std::{ + net::TcpListener, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +fn connect_target( + endpoint: &str, +) -> originweave_core::WebDriverBiDiWebSocketConnectTarget { + let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); + assert!(admitted.is_ok(), "{admitted:?}"); + let Ok(admitted) = admitted else { + unreachable!("asserted valid endpoint") + }; + + let correlated = admitted.correlate_session_id(SESSION_ID); + assert!(correlated.is_ok(), "{correlated:?}"); + let Ok(correlated) = correlated else { + unreachable!("asserted correlated endpoint") + }; + + let target = correlated.into_explicit_connect_target(); + assert!(target.is_ok(), "{target:?}"); + let Ok(target) = target else { + unreachable!("asserted literal loopback target") + }; + target +} + +#[test] +fn exact_loopback_target_opens_one_verified_bidi_tcp_stream() { + let listener = TcpListener::bind(("127.0.0.1", 0)); + assert!(listener.is_ok(), "{listener:?}"); + let Ok(listener) = listener else { + return; + }; + let local_addr = listener.local_addr(); + assert!(local_addr.is_ok(), "{local_addr:?}"); + let Ok(local_addr) = local_addr else { + return; + }; + + let server = thread::spawn(move || listener.accept().map(|_| ())); + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let target = connect_target(&endpoint); + let plan = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1); + assert!(plan.is_ok(), "{plan:?}"); + let Ok(plan) = plan else { + return; + }; + + let connection = plan.connect(); + assert!(connection.is_ok(), "{connection:?}"); + let Ok(connection) = connection else { + return; + }; + + assert_eq!(connection.verified_peer().socket_addr(), local_addr); + assert!(!connection.verified_peer().requires_tls()); + assert_eq!(connection.verified_peer().session_id(), SESSION_ID); + assert_eq!(connection.attempt_number(), 1); + assert_eq!(connection.connect_timeout(), Duration::from_secs(1)); + + let server_result = server.join(); + assert!(server_result.is_ok(), "{server_result:?}"); + if let Ok(accept_result) = server_result { + assert!(accept_result.is_ok(), "{accept_result:?}"); + } +} + +#[test] +fn bidi_tcp_plan_rejects_invalid_retry_settings_before_io() { + let endpoint = format!("wss://127.0.0.1:9443/session/{SESSION_ID}"); + let zero_timeout = WebDriverBiDiTcpConnectionPlan::new( + connect_target(&endpoint), + Duration::ZERO, + 1, + ); + assert!(matches!( + zero_timeout, + Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { .. }) + )); + + let zero_attempts = WebDriverBiDiTcpConnectionPlan::new( + connect_target(&endpoint), + Duration::from_secs(1), + 0, + ); + assert!(matches!( + zero_attempts, + Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { .. }) + )); +} From aef0ac7763c1ed288bf17580162cf37e42348d04 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:32:20 -0700 Subject: [PATCH 097/118] test(network): format BiDi TCP RED contract --- .../tests/webdriver_bidi_tcp_connection.rs | 28 +++++-------------- 1 file changed, 7 insertions(+), 21 deletions(-) diff --git a/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs b/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs index 5cfd49783..bfc80457b 100644 --- a/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs +++ b/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs @@ -1,19 +1,11 @@ -use std::{ - net::TcpListener, - thread, - time::Duration, -}; +use std::{net::TcpListener, thread, time::Duration}; use originweave_core::WebDriverBiDiWebSocketEndpoint; -use originweave_network::{ - WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, -}; +use originweave_network::{WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan}; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; -fn connect_target( - endpoint: &str, -) -> originweave_core::WebDriverBiDiWebSocketConnectTarget { +fn connect_target(endpoint: &str) -> originweave_core::WebDriverBiDiWebSocketConnectTarget { let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); assert!(admitted.is_ok(), "{admitted:?}"); let Ok(admitted) = admitted else { @@ -78,21 +70,15 @@ fn exact_loopback_target_opens_one_verified_bidi_tcp_stream() { #[test] fn bidi_tcp_plan_rejects_invalid_retry_settings_before_io() { let endpoint = format!("wss://127.0.0.1:9443/session/{SESSION_ID}"); - let zero_timeout = WebDriverBiDiTcpConnectionPlan::new( - connect_target(&endpoint), - Duration::ZERO, - 1, - ); + let zero_timeout = + WebDriverBiDiTcpConnectionPlan::new(connect_target(&endpoint), Duration::ZERO, 1); assert!(matches!( zero_timeout, Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { .. }) )); - let zero_attempts = WebDriverBiDiTcpConnectionPlan::new( - connect_target(&endpoint), - Duration::from_secs(1), - 0, - ); + let zero_attempts = + WebDriverBiDiTcpConnectionPlan::new(connect_target(&endpoint), Duration::from_secs(1), 0); assert!(matches!( zero_attempts, Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { .. }) From 1de304b0d944b7c1ac9412b7cc84d787cb34b0ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:35:18 -0700 Subject: [PATCH 098/118] feat(network): connect exact BiDi loopback transport --- .../src/webdriver_bidi_connection.rs | 691 ++++++++++++++++++ 1 file changed, 691 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_connection.rs diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs new file mode 100644 index 000000000..0be687193 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -0,0 +1,691 @@ +use std::{fmt, io, net::SocketAddr, net::TcpStream, time::Duration}; + +use originweave_core::{ + VerifiedWebDriverBiDiSocketPeer, WebDriverBiDiSocketPeerVerificationError, + WebDriverBiDiWebSocketConnectTarget, +}; + +use crate::connection::{MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS}; + +fn is_retryable_connect_error(kind: io::ErrorKind) -> bool { + matches!( + kind, + io::ErrorKind::TimedOut + | io::ErrorKind::ConnectionRefused + | io::ErrorKind::ConnectionReset + | io::ErrorKind::ConnectionAborted + | io::ErrorKind::Interrupted + ) +} + +/// Single-use authority to open one exact WebDriver BiDi loopback TCP destination. +/// +/// The plan consumes a session-correlated, no-DNS [`WebDriverBiDiWebSocketConnectTarget`] +/// produced by `originweave-core`. It applies the same bounded per-attempt timeout and retry +/// ceilings as the general direct-network connector, opens only the exact [`SocketAddr`] carried by +/// that target, and does not expose the stream until the operating system's observed peer has been +/// verified by the consumed target. +/// +/// This boundary performs no DNS lookup, proxy or PAC routing, Chromium/ChromeDriver process +/// authentication, TLS negotiation, WebSocket upgrade, BiDi framing, browser policy decision, or +/// Agent-authority grant. +#[derive(Debug)] +pub struct WebDriverBiDiTcpConnectionPlan { + target: WebDriverBiDiWebSocketConnectTarget, + connect_timeout: Duration, + maximum_attempts: u8, +} + +impl WebDriverBiDiTcpConnectionPlan { + /// Validate one bounded exact-loopback connection plan without performing network I/O. + pub fn new( + target: WebDriverBiDiWebSocketConnectTarget, + connect_timeout: Duration, + maximum_attempts: u8, + ) -> Result { + if connect_timeout.is_zero() || connect_timeout > MAX_CONNECT_TIMEOUT { + return Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { + connect_timeout, + maximum_timeout: MAX_CONNECT_TIMEOUT, + }); + } + if maximum_attempts == 0 || maximum_attempts > MAX_CONNECTION_ATTEMPTS { + return Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { + attempt_count: maximum_attempts, + maximum_attempts: MAX_CONNECTION_ATTEMPTS, + }); + } + + Ok(Self { + target, + connect_timeout, + maximum_attempts, + }) + } + + /// Open the exact approved loopback socket and expose it only after peer verification. + /// + /// Retry is limited to transport errors that can occur transiently while a local browser driver + /// listener is becoming ready. Peer-inspection and peer-mismatch failures are integrity failures + /// and therefore fail closed without retry or fallback. + pub fn connect(self) -> Result { + self.connect_with(&SystemWebDriverBiDiConnector) + } + + fn connect_with( + self, + connector: &dyn WebDriverBiDiSocketConnector, + ) -> Result { + let socket_address = self.target.socket_addr(); + let connect_timeout = self.connect_timeout; + let maximum_attempts = self.maximum_attempts; + let target = self.target; + let mut attempt_number = 1; + + loop { + match connector.connect_timeout(&socket_address, connect_timeout) { + Ok(stream) => { + let observed_peer = connector.peer_addr(&stream).map_err(|source| { + WebDriverBiDiTcpConnectionError::PeerInspectionFailed { + socket_address, + attempt_number, + source, + } + })?; + let verified_peer = target.verify_connected_peer(observed_peer).map_err( + |source| WebDriverBiDiTcpConnectionError::PeerMismatch { + attempt_number, + source, + }, + )?; + return Ok(WebDriverBiDiTcpConnection { + stream, + verified_peer, + attempt_number, + connect_timeout, + }); + } + Err(source) + if is_retryable_connect_error(source.kind()) + && attempt_number < maximum_attempts => + { + attempt_number += 1; + } + Err(source) => { + if source.kind() == io::ErrorKind::TimedOut { + return Err(WebDriverBiDiTcpConnectionError::ConnectionTimedOut { + socket_address, + attempt_count: attempt_number, + connect_timeout, + source, + }); + } + return Err(WebDriverBiDiTcpConnectionError::ConnectionFailed { + socket_address, + attempt_count: attempt_number, + source, + }); + } + } + } + } +} + +trait WebDriverBiDiSocketConnector { + fn connect_timeout( + &self, + socket_address: &SocketAddr, + timeout: Duration, + ) -> io::Result; + + fn peer_addr(&self, stream: &TcpStream) -> io::Result; +} + +struct SystemWebDriverBiDiConnector; + +impl WebDriverBiDiSocketConnector for SystemWebDriverBiDiConnector { + fn connect_timeout( + &self, + socket_address: &SocketAddr, + timeout: Duration, + ) -> io::Result { + TcpStream::connect_timeout(socket_address, timeout) + } + + fn peer_addr(&self, stream: &TcpStream) -> io::Result { + stream.peer_addr() + } +} + +/// Established WebDriver BiDi TCP stream whose observed peer matched the approved target exactly. +/// +/// This wrapper proves only exact transport-destination equality for one bounded connection. The +/// caller must still establish any required TLS channel, complete a WebSocket handshake, bind the +/// transport to the expected browser process/session, and pass separate action-policy checks. +#[derive(Debug)] +pub struct WebDriverBiDiTcpConnection { + stream: TcpStream, + verified_peer: VerifiedWebDriverBiDiSocketPeer, + attempt_number: u8, + connect_timeout: Duration, +} + +impl WebDriverBiDiTcpConnection { + /// Borrow the verified TCP stream. + #[must_use] + pub const fn stream(&self) -> &TcpStream { + &self.stream + } + + /// Borrow the session-correlated exact peer evidence consumed by this connection. + #[must_use] + pub const fn verified_peer(&self) -> &VerifiedWebDriverBiDiSocketPeer { + &self.verified_peer + } + + /// Return the one-based bounded attempt on which the connection succeeded. + #[must_use] + pub const fn attempt_number(&self) -> u8 { + self.attempt_number + } + + /// Return the per-attempt timeout applied while establishing this connection. + #[must_use] + pub const fn connect_timeout(&self) -> Duration { + self.connect_timeout + } +} + +/// Deterministic failures while establishing one bounded WebDriver BiDi TCP transport. +#[derive(Debug)] +pub enum WebDriverBiDiTcpConnectionError { + /// The requested timeout was zero or exceeded [`MAX_CONNECT_TIMEOUT`]. + InvalidConnectTimeout { + /// The rejected timeout. + connect_timeout: Duration, + /// The largest accepted per-attempt timeout. + maximum_timeout: Duration, + }, + /// The requested attempt count was outside `1..=MAX_CONNECTION_ATTEMPTS`. + InvalidAttemptCount { + /// The rejected attempt count. + attempt_count: u8, + /// The largest accepted attempt count. + maximum_attempts: u8, + }, + /// The final bounded connection attempt timed out. + ConnectionTimedOut { + /// Exact approved socket address submitted to the operating system. + socket_address: SocketAddr, + /// Number of attempts completed before failure. + attempt_count: u8, + /// Per-attempt timeout used by the plan. + connect_timeout: Duration, + /// Final operating-system timeout error. + source: io::Error, + }, + /// The final bounded connection attempt failed without a timeout. + ConnectionFailed { + /// Exact approved socket address submitted to the operating system. + socket_address: SocketAddr, + /// Number of attempts completed before failure. + attempt_count: u8, + /// Final operating-system connection error. + source: io::Error, + }, + /// The established stream did not reveal an operating-system peer address. + PeerInspectionFailed { + /// Exact approved socket address submitted to the operating system. + socket_address: SocketAddr, + /// One-based attempt that established the stream. + attempt_number: u8, + /// Operating-system peer-inspection error. + source: io::Error, + }, + /// The established stream reported a peer other than the exact approved BiDi target. + PeerMismatch { + /// One-based attempt that established the stream. + attempt_number: u8, + /// Typed core peer-verification failure preserving expected and actual socket addresses. + source: WebDriverBiDiSocketPeerVerificationError, + }, +} + +impl WebDriverBiDiTcpConnectionError { + /// Return the number of transport attempts associated with this failure, when applicable. + #[must_use] + pub const fn attempt_count(&self) -> Option { + match self { + Self::ConnectionTimedOut { attempt_count, .. } + | Self::ConnectionFailed { attempt_count, .. } => Some(*attempt_count), + Self::PeerInspectionFailed { attempt_number, .. } + | Self::PeerMismatch { attempt_number, .. } => Some(*attempt_number), + Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, + } + } +} + +impl fmt::Display for WebDriverBiDiTcpConnectionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidConnectTimeout { + connect_timeout, + maximum_timeout, + } => write!( + formatter, + "WebDriver BiDi connect timeout {connect_timeout:?} is outside 1ns..={maximum_timeout:?}", + ), + Self::InvalidAttemptCount { + attempt_count, + maximum_attempts, + } => write!( + formatter, + "WebDriver BiDi connection attempt count {attempt_count} is outside 1..={maximum_attempts}", + ), + Self::ConnectionTimedOut { + socket_address, + attempt_count, + connect_timeout, + .. + } => write!( + formatter, + "WebDriver BiDi TCP connection to {socket_address} timed out after {attempt_count} attempts with per-attempt timeout {connect_timeout:?}", + ), + Self::ConnectionFailed { + socket_address, + attempt_count, + .. + } => write!( + formatter, + "WebDriver BiDi TCP connection to {socket_address} failed after {attempt_count} attempts", + ), + Self::PeerInspectionFailed { + socket_address, + attempt_number, + .. + } => write!( + formatter, + "WebDriver BiDi TCP peer inspection failed for {socket_address} on attempt {attempt_number}", + ), + Self::PeerMismatch { attempt_number, .. } => write!( + formatter, + "WebDriver BiDi TCP peer did not match the approved target on attempt {attempt_number}", + ), + } + } +} + +impl std::error::Error for WebDriverBiDiTcpConnectionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ConnectionTimedOut { source, .. } + | Self::ConnectionFailed { source, .. } + | Self::PeerInspectionFailed { source, .. } => Some(source), + Self::PeerMismatch { source, .. } => Some(source), + Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, + } + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use std::{ + cell::{Cell, RefCell}, + collections::VecDeque, + error::Error, + net::{TcpListener, TcpStream}, + }; + + use originweave_core::WebDriverBiDiWebSocketEndpoint; + + use super::*; + + const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + const SOCKET_ADDRESS: SocketAddr = SocketAddr::from(([127, 0, 0, 1], 9515)); + + enum ConnectOutcome { + Success(TcpStream), + Error(io::ErrorKind), + } + + enum PeerOutcome { + Address(SocketAddr), + Error(io::ErrorKind), + } + + struct FakeConnector { + connect_outcomes: RefCell>, + peer_outcomes: RefCell>, + connect_calls: Cell, + peer_calls: Cell, + } + + impl FakeConnector { + fn new(connect_outcomes: Vec, peer_outcomes: Vec) -> Self { + Self { + connect_outcomes: RefCell::new(connect_outcomes.into()), + peer_outcomes: RefCell::new(peer_outcomes.into()), + connect_calls: Cell::new(0), + peer_calls: Cell::new(0), + } + } + } + + impl WebDriverBiDiSocketConnector for FakeConnector { + fn connect_timeout( + &self, + _socket_address: &SocketAddr, + _timeout: Duration, + ) -> io::Result { + self.connect_calls.set(self.connect_calls.get() + 1); + match self + .connect_outcomes + .borrow_mut() + .pop_front() + .expect("test must provide a connection outcome") + { + ConnectOutcome::Success(stream) => Ok(stream), + ConnectOutcome::Error(kind) => Err(io::Error::from(kind)), + } + } + + fn peer_addr(&self, _stream: &TcpStream) -> io::Result { + self.peer_calls.set(self.peer_calls.get() + 1); + match self + .peer_outcomes + .borrow_mut() + .pop_front() + .expect("test must provide a peer outcome") + { + PeerOutcome::Address(address) => Ok(address), + PeerOutcome::Error(kind) => Err(io::Error::from(kind)), + } + } + } + + fn loopback_stream() -> TcpStream { + let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind loopback listener"); + let address = listener.local_addr().expect("read loopback listener address"); + let client = TcpStream::connect(address).expect("connect loopback client"); + let (server, _) = listener.accept().expect("accept loopback client"); + drop(server); + client + } + + fn connect_target(secure: bool) -> WebDriverBiDiWebSocketConnectTarget { + let scheme = if secure { "wss" } else { "ws" }; + let endpoint = format!("{scheme}://127.0.0.1:9515/session/{SESSION_ID}"); + let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint).expect("admit endpoint"); + let correlated = admitted + .correlate_session_id(SESSION_ID) + .expect("correlate endpoint"); + correlated + .into_explicit_connect_target() + .expect("derive explicit connect target") + } + + fn plan(maximum_attempts: u8) -> WebDriverBiDiTcpConnectionPlan { + WebDriverBiDiTcpConnectionPlan::new( + connect_target(false), + Duration::from_millis(250), + maximum_attempts, + ) + .expect("valid test plan") + } + + #[test] + fn validates_timeout_and_attempt_bounds_before_io() { + let zero_timeout = WebDriverBiDiTcpConnectionPlan::new( + connect_target(false), + Duration::ZERO, + 1, + ); + assert!(matches!( + zero_timeout, + Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { .. }) + )); + + let excessive_timeout = WebDriverBiDiTcpConnectionPlan::new( + connect_target(false), + MAX_CONNECT_TIMEOUT + Duration::from_nanos(1), + 1, + ); + assert!(matches!( + excessive_timeout, + Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { .. }) + )); + + let zero_attempts = WebDriverBiDiTcpConnectionPlan::new( + connect_target(false), + Duration::from_millis(250), + 0, + ); + assert!(matches!( + zero_attempts, + Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { .. }) + )); + + let excessive_attempts = WebDriverBiDiTcpConnectionPlan::new( + connect_target(false), + Duration::from_millis(250), + MAX_CONNECTION_ATTEMPTS + 1, + ); + assert!(matches!( + excessive_attempts, + Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { .. }) + )); + } + + #[test] + fn verified_peer_is_required_before_stream_exposure() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Success(loopback_stream())], + vec![PeerOutcome::Address(SOCKET_ADDRESS)], + ); + let connection = WebDriverBiDiTcpConnectionPlan::new( + connect_target(true), + Duration::from_millis(250), + 1, + ) + .expect("valid plan") + .connect_with(&connector) + .expect("verified connection"); + + assert!(connection.stream().peer_addr().is_ok()); + assert_eq!(connection.verified_peer().socket_addr(), SOCKET_ADDRESS); + assert!(connection.verified_peer().requires_tls()); + assert_eq!(connection.verified_peer().session_id(), SESSION_ID); + assert_eq!(connection.attempt_number(), 1); + assert_eq!(connection.connect_timeout(), Duration::from_millis(250)); + assert_eq!(connector.connect_calls.get(), 1); + assert_eq!(connector.peer_calls.get(), 1); + } + + #[test] + fn all_recoverable_connect_kinds_can_retry_once() { + for kind in [ + io::ErrorKind::TimedOut, + io::ErrorKind::ConnectionRefused, + io::ErrorKind::ConnectionReset, + io::ErrorKind::ConnectionAborted, + io::ErrorKind::Interrupted, + ] { + assert!(is_retryable_connect_error(kind)); + let connector = FakeConnector::new( + vec![ + ConnectOutcome::Error(kind), + ConnectOutcome::Success(loopback_stream()), + ], + vec![PeerOutcome::Address(SOCKET_ADDRESS)], + ); + let connection = plan(2) + .connect_with(&connector) + .expect("second bounded attempt succeeds"); + assert_eq!(connection.attempt_number(), 2); + assert_eq!(connector.connect_calls.get(), 2); + assert_eq!(connector.peer_calls.get(), 1); + } + assert!(!is_retryable_connect_error(io::ErrorKind::PermissionDenied)); + } + + #[test] + fn final_timeout_preserves_source_and_attempt_count() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Error(io::ErrorKind::TimedOut)], + Vec::new(), + ); + let error = plan(1) + .connect_with(&connector) + .expect_err("timeout must fail closed"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::ConnectionTimedOut { + attempt_count: 1, + .. + } + )); + assert!(error.source().is_some()); + assert_eq!(error.attempt_count(), Some(1)); + } + + #[test] + fn exhausted_retryable_non_timeout_error_is_connection_failure() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Error(io::ErrorKind::ConnectionRefused)], + Vec::new(), + ); + let error = plan(1) + .connect_with(&connector) + .expect_err("refusal must fail after the bounded final attempt"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::ConnectionFailed { + attempt_count: 1, + .. + } + )); + assert!(error.source().is_some()); + } + + #[test] + fn non_retryable_connection_error_fails_without_retry() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Error(io::ErrorKind::PermissionDenied)], + Vec::new(), + ); + let error = plan(MAX_CONNECTION_ATTEMPTS) + .connect_with(&connector) + .expect_err("permission failure must not retry"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::ConnectionFailed { + attempt_count: 1, + .. + } + )); + assert_eq!(connector.connect_calls.get(), 1); + } + + #[test] + fn peer_inspection_failure_is_not_retried() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Success(loopback_stream())], + vec![PeerOutcome::Error(io::ErrorKind::NotConnected)], + ); + let error = plan(MAX_CONNECTION_ATTEMPTS) + .connect_with(&connector) + .expect_err("peer inspection failure must fail closed"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::PeerInspectionFailed { + attempt_number: 1, + .. + } + )); + assert_eq!(connector.connect_calls.get(), 1); + assert_eq!(connector.peer_calls.get(), 1); + assert!(error.source().is_some()); + } + + #[test] + fn peer_mismatch_is_not_retried_or_converted_to_success() { + let wrong_peer = SocketAddr::from(([127, 0, 0, 1], 9516)); + let connector = FakeConnector::new( + vec![ConnectOutcome::Success(loopback_stream())], + vec![PeerOutcome::Address(wrong_peer)], + ); + let error = plan(MAX_CONNECTION_ATTEMPTS) + .connect_with(&connector) + .expect_err("peer mismatch must fail closed"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::PeerMismatch { + attempt_number: 1, + .. + } + )); + assert_eq!(connector.connect_calls.get(), 1); + assert_eq!(connector.peer_calls.get(), 1); + assert!(error.source().is_some()); + } + + #[test] + fn error_display_source_and_attempt_contracts_cover_every_variant() { + let mismatch = connect_target(false) + .verify_connected_peer(SocketAddr::from(([127, 0, 0, 1], 9516))) + .expect_err("wrong peer must fail"); + let errors = [ + WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { + connect_timeout: Duration::ZERO, + maximum_timeout: MAX_CONNECT_TIMEOUT, + }, + WebDriverBiDiTcpConnectionError::InvalidAttemptCount { + attempt_count: 0, + maximum_attempts: MAX_CONNECTION_ATTEMPTS, + }, + WebDriverBiDiTcpConnectionError::ConnectionTimedOut { + socket_address: SOCKET_ADDRESS, + attempt_count: 2, + connect_timeout: Duration::from_millis(250), + source: io::Error::from(io::ErrorKind::TimedOut), + }, + WebDriverBiDiTcpConnectionError::ConnectionFailed { + socket_address: SOCKET_ADDRESS, + attempt_count: 3, + source: io::Error::from(io::ErrorKind::ConnectionRefused), + }, + WebDriverBiDiTcpConnectionError::PeerInspectionFailed { + socket_address: SOCKET_ADDRESS, + attempt_number: 1, + source: io::Error::from(io::ErrorKind::NotConnected), + }, + WebDriverBiDiTcpConnectionError::PeerMismatch { + attempt_number: 1, + source: mismatch, + }, + ]; + + let messages: Vec = errors.iter().map(ToString::to_string).collect(); + assert!(messages[0].contains("outside 1ns")); + assert!(messages[1].contains("attempt count 0")); + assert!(messages[2].contains("timed out after 2 attempts")); + assert!(messages[3].contains("failed after 3 attempts")); + assert!(messages[4].contains("peer inspection failed")); + assert!(messages[5].contains("did not match the approved target")); + + assert_eq!(errors[0].attempt_count(), None); + assert_eq!(errors[1].attempt_count(), None); + assert_eq!(errors[2].attempt_count(), Some(2)); + assert_eq!(errors[3].attempt_count(), Some(3)); + assert_eq!(errors[4].attempt_count(), Some(1)); + assert_eq!(errors[5].attempt_count(), Some(1)); + assert!(errors[0].source().is_none()); + assert!(errors[1].source().is_none()); + assert!(errors[2].source().is_some()); + assert!(errors[3].source().is_some()); + assert!(errors[4].source().is_some()); + assert!(errors[5].source().is_some()); + } +} From ccb7d31dfe7654bab800d463c2391cc1a19c7d74 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:36:54 -0700 Subject: [PATCH 099/118] feat(network): export bounded BiDi TCP transport --- crates/originweave-network/src/lib.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index d5b26c1c3..67f85c973 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -1,15 +1,22 @@ //! Direct-only policy-bound TCP connection authority for OriginWeave. //! -//! The crate consumes a validated connection plan, opens one exact socket -//! address without hostname resolution or proxy inheritance, verifies the -//! operating-system peer, and emits credential-free evidence. +//! The crate consumes validated connection plans, opens exact socket addresses +//! without hostname resolution or proxy inheritance, verifies operating-system +//! peers before exposing transport I/O, and emits credential-free evidence. +//! It also bridges a session-correlated WebDriver BiDi loopback target from +//! `originweave-core` into one bounded exact TCP connection without granting +//! browser, WebSocket, TLS, policy, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] mod connection; +mod webdriver_bidi_connection; pub use connection::{ ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, NetworkError, SocketConnectionEvidence, }; +pub use webdriver_bidi_connection::{ + WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, +}; From b4ce10df0f3a76794a08b91b64cf242f9591c1e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:40:16 -0700 Subject: [PATCH 100/118] refactor(network): isolate BiDi transport errors --- .../src/webdriver_bidi_connection/error.rs | 136 ++++++++++++++++++ 1 file changed, 136 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_connection/error.rs diff --git a/crates/originweave-network/src/webdriver_bidi_connection/error.rs b/crates/originweave-network/src/webdriver_bidi_connection/error.rs new file mode 100644 index 000000000..613f3101a --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_connection/error.rs @@ -0,0 +1,136 @@ +use std::{fmt, io, net::SocketAddr, time::Duration}; + +use originweave_core::WebDriverBiDiSocketPeerVerificationError; + +use crate::connection::{MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS}; + +/// Deterministic failures while establishing one bounded WebDriver BiDi TCP transport. +#[derive(Debug)] +pub enum WebDriverBiDiTcpConnectionError { + /// The requested timeout was zero or exceeded [`MAX_CONNECT_TIMEOUT`]. + InvalidConnectTimeout { + /// The rejected timeout. + connect_timeout: Duration, + /// The largest accepted per-attempt timeout. + maximum_timeout: Duration, + }, + /// The requested attempt count was outside `1..=MAX_CONNECTION_ATTEMPTS`. + InvalidAttemptCount { + /// The rejected attempt count. + attempt_count: u8, + /// The largest accepted attempt count. + maximum_attempts: u8, + }, + /// The final bounded connection attempt timed out. + ConnectionTimedOut { + /// Exact approved socket address submitted to the operating system. + socket_address: SocketAddr, + /// Number of attempts completed before failure. + attempt_count: u8, + /// Per-attempt timeout used by the plan. + connect_timeout: Duration, + /// Final operating-system timeout error. + source: io::Error, + }, + /// The final bounded connection attempt failed without a timeout. + ConnectionFailed { + /// Exact approved socket address submitted to the operating system. + socket_address: SocketAddr, + /// Number of attempts completed before failure. + attempt_count: u8, + /// Final operating-system connection error. + source: io::Error, + }, + /// The established stream did not reveal an operating-system peer address. + PeerInspectionFailed { + /// Exact approved socket address submitted to the operating system. + socket_address: SocketAddr, + /// One-based attempt that established the stream. + attempt_number: u8, + /// Operating-system peer-inspection error. + source: io::Error, + }, + /// The established stream reported a peer other than the exact approved BiDi target. + PeerMismatch { + /// One-based attempt that established the stream. + attempt_number: u8, + /// Typed core peer-verification failure preserving expected and actual socket addresses. + source: WebDriverBiDiSocketPeerVerificationError, + }, +} + +impl WebDriverBiDiTcpConnectionError { + /// Return the number of transport attempts associated with this failure, when applicable. + #[must_use] + pub const fn attempt_count(&self) -> Option { + match self { + Self::ConnectionTimedOut { attempt_count, .. } + | Self::ConnectionFailed { attempt_count, .. } => Some(*attempt_count), + Self::PeerInspectionFailed { attempt_number, .. } + | Self::PeerMismatch { attempt_number, .. } => Some(*attempt_number), + Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, + } + } +} + +impl fmt::Display for WebDriverBiDiTcpConnectionError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidConnectTimeout { + connect_timeout, + maximum_timeout, + } => write!( + formatter, + "WebDriver BiDi connect timeout {connect_timeout:?} is outside 1ns..={maximum_timeout:?}", + ), + Self::InvalidAttemptCount { + attempt_count, + maximum_attempts, + } => write!( + formatter, + "WebDriver BiDi connection attempt count {attempt_count} is outside 1..={maximum_attempts}", + ), + Self::ConnectionTimedOut { + socket_address, + attempt_count, + connect_timeout, + .. + } => write!( + formatter, + "WebDriver BiDi TCP connection to {socket_address} timed out after {attempt_count} attempts with per-attempt timeout {connect_timeout:?}", + ), + Self::ConnectionFailed { + socket_address, + attempt_count, + .. + } => write!( + formatter, + "WebDriver BiDi TCP connection to {socket_address} failed after {attempt_count} attempts", + ), + Self::PeerInspectionFailed { + socket_address, + attempt_number, + .. + } => write!( + formatter, + "WebDriver BiDi TCP peer inspection failed for {socket_address} on attempt {attempt_number}", + ), + Self::PeerMismatch { attempt_number, .. } => write!( + formatter, + "WebDriver BiDi TCP peer did not match the approved target on attempt {attempt_number}", + ), + } + } +} + +impl std::error::Error for WebDriverBiDiTcpConnectionError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::ConnectionTimedOut { source, .. } + | Self::ConnectionFailed { source, .. } + | Self::PeerInspectionFailed { source, .. } => Some(source), + Self::PeerMismatch { source, .. } => Some(source), + Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, + } + } +} From 8ed075c262df6b7338074f8eebbd748248628213 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:40:57 -0700 Subject: [PATCH 101/118] test(network): isolate BiDi transport resilience coverage --- .../src/webdriver_bidi_connection/tests.rs | 367 ++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 crates/originweave-network/src/webdriver_bidi_connection/tests.rs diff --git a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs new file mode 100644 index 000000000..4720493f6 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs @@ -0,0 +1,367 @@ +#![allow(clippy::expect_used)] + +use std::{ + cell::{Cell, RefCell}, + collections::VecDeque, + error::Error, + io, + net::{SocketAddr, TcpListener, TcpStream}, + time::Duration, +}; + +use originweave_core::{WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketEndpoint}; + +use super::{ + is_retryable_connect_error, WebDriverBiDiSocketConnector, WebDriverBiDiTcpConnectionError, + WebDriverBiDiTcpConnectionPlan, +}; +use crate::connection::{MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; + +fn socket_address() -> SocketAddr { + SocketAddr::from(([127, 0, 0, 1], 9515)) +} + +enum ConnectOutcome { + Success(TcpStream), + Error(io::ErrorKind), +} + +enum PeerOutcome { + Address(SocketAddr), + Error(io::ErrorKind), +} + +struct FakeConnector { + connect_outcomes: RefCell>, + peer_outcomes: RefCell>, + connect_calls: Cell, + peer_calls: Cell, +} + +impl FakeConnector { + fn new(connect_outcomes: Vec, peer_outcomes: Vec) -> Self { + Self { + connect_outcomes: RefCell::new(connect_outcomes.into()), + peer_outcomes: RefCell::new(peer_outcomes.into()), + connect_calls: Cell::new(0), + peer_calls: Cell::new(0), + } + } +} + +impl WebDriverBiDiSocketConnector for FakeConnector { + fn connect_timeout( + &self, + _socket_address: &SocketAddr, + _timeout: Duration, + ) -> io::Result { + self.connect_calls.set(self.connect_calls.get() + 1); + match self + .connect_outcomes + .borrow_mut() + .pop_front() + .expect("test must provide a connection outcome") + { + ConnectOutcome::Success(stream) => Ok(stream), + ConnectOutcome::Error(kind) => Err(io::Error::from(kind)), + } + } + + fn peer_addr(&self, _stream: &TcpStream) -> io::Result { + self.peer_calls.set(self.peer_calls.get() + 1); + match self + .peer_outcomes + .borrow_mut() + .pop_front() + .expect("test must provide a peer outcome") + { + PeerOutcome::Address(address) => Ok(address), + PeerOutcome::Error(kind) => Err(io::Error::from(kind)), + } + } +} + +fn loopback_stream() -> TcpStream { + let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind loopback listener"); + let address = listener + .local_addr() + .expect("read loopback listener address"); + let client = TcpStream::connect(address).expect("connect loopback client"); + let (server, _) = listener.accept().expect("accept loopback client"); + drop(server); + client +} + +fn connect_target(secure: bool) -> WebDriverBiDiWebSocketConnectTarget { + let scheme = if secure { "wss" } else { "ws" }; + let endpoint = format!("{scheme}://127.0.0.1:9515/session/{SESSION_ID}"); + let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint).expect("admit endpoint"); + let correlated = admitted + .correlate_session_id(SESSION_ID) + .expect("correlate endpoint"); + correlated + .into_explicit_connect_target() + .expect("derive explicit connect target") +} + +fn plan(maximum_attempts: u8) -> WebDriverBiDiTcpConnectionPlan { + WebDriverBiDiTcpConnectionPlan::new( + connect_target(false), + Duration::from_millis(250), + maximum_attempts, + ) + .expect("valid test plan") +} + +#[test] +fn validates_timeout_and_attempt_bounds_before_io() { + let zero_timeout = + WebDriverBiDiTcpConnectionPlan::new(connect_target(false), Duration::ZERO, 1); + assert!(matches!( + zero_timeout, + Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { .. }) + )); + + let excessive_timeout = WebDriverBiDiTcpConnectionPlan::new( + connect_target(false), + MAX_CONNECT_TIMEOUT + Duration::from_nanos(1), + 1, + ); + assert!(matches!( + excessive_timeout, + Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { .. }) + )); + + let zero_attempts = WebDriverBiDiTcpConnectionPlan::new( + connect_target(false), + Duration::from_millis(250), + 0, + ); + assert!(matches!( + zero_attempts, + Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { .. }) + )); + + let excessive_attempts = WebDriverBiDiTcpConnectionPlan::new( + connect_target(false), + Duration::from_millis(250), + MAX_CONNECTION_ATTEMPTS + 1, + ); + assert!(matches!( + excessive_attempts, + Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { .. }) + )); +} + +#[test] +fn verified_peer_is_required_before_stream_exposure() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Success(loopback_stream())], + vec![PeerOutcome::Address(socket_address())], + ); + let connection = WebDriverBiDiTcpConnectionPlan::new( + connect_target(true), + Duration::from_millis(250), + 1, + ) + .expect("valid plan") + .connect_with(&connector) + .expect("verified connection"); + + assert!(connection.stream().peer_addr().is_ok()); + assert_eq!(connection.verified_peer().socket_addr(), socket_address()); + assert!(connection.verified_peer().requires_tls()); + assert_eq!(connection.verified_peer().session_id(), SESSION_ID); + assert_eq!(connection.attempt_number(), 1); + assert_eq!(connection.connect_timeout(), Duration::from_millis(250)); + assert_eq!(connector.connect_calls.get(), 1); + assert_eq!(connector.peer_calls.get(), 1); +} + +#[test] +fn all_recoverable_connect_kinds_can_retry_once() { + for kind in [ + io::ErrorKind::TimedOut, + io::ErrorKind::ConnectionRefused, + io::ErrorKind::ConnectionReset, + io::ErrorKind::ConnectionAborted, + io::ErrorKind::Interrupted, + ] { + assert!(is_retryable_connect_error(kind)); + let connector = FakeConnector::new( + vec![ + ConnectOutcome::Error(kind), + ConnectOutcome::Success(loopback_stream()), + ], + vec![PeerOutcome::Address(socket_address())], + ); + let connection = plan(2) + .connect_with(&connector) + .expect("second bounded attempt succeeds"); + assert_eq!(connection.attempt_number(), 2); + assert_eq!(connector.connect_calls.get(), 2); + assert_eq!(connector.peer_calls.get(), 1); + } + assert!(!is_retryable_connect_error(io::ErrorKind::PermissionDenied)); +} + +#[test] +fn final_timeout_preserves_source_and_attempt_count() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Error(io::ErrorKind::TimedOut)], + Vec::new(), + ); + let error = plan(1) + .connect_with(&connector) + .expect_err("timeout must fail closed"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::ConnectionTimedOut { + attempt_count: 1, + .. + } + )); + assert!(error.source().is_some()); + assert_eq!(error.attempt_count(), Some(1)); +} + +#[test] +fn exhausted_retryable_non_timeout_error_is_connection_failure() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Error(io::ErrorKind::ConnectionRefused)], + Vec::new(), + ); + let error = plan(1) + .connect_with(&connector) + .expect_err("refusal must fail after the bounded final attempt"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::ConnectionFailed { + attempt_count: 1, + .. + } + )); + assert!(error.source().is_some()); +} + +#[test] +fn non_retryable_connection_error_fails_without_retry() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Error(io::ErrorKind::PermissionDenied)], + Vec::new(), + ); + let error = plan(MAX_CONNECTION_ATTEMPTS) + .connect_with(&connector) + .expect_err("permission failure must not retry"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::ConnectionFailed { + attempt_count: 1, + .. + } + )); + assert_eq!(connector.connect_calls.get(), 1); +} + +#[test] +fn peer_inspection_failure_is_not_retried() { + let connector = FakeConnector::new( + vec![ConnectOutcome::Success(loopback_stream())], + vec![PeerOutcome::Error(io::ErrorKind::NotConnected)], + ); + let error = plan(MAX_CONNECTION_ATTEMPTS) + .connect_with(&connector) + .expect_err("peer inspection failure must fail closed"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::PeerInspectionFailed { + attempt_number: 1, + .. + } + )); + assert_eq!(connector.connect_calls.get(), 1); + assert_eq!(connector.peer_calls.get(), 1); + assert!(error.source().is_some()); +} + +#[test] +fn peer_mismatch_is_not_retried_or_converted_to_success() { + let wrong_peer = SocketAddr::from(([127, 0, 0, 1], 9516)); + let connector = FakeConnector::new( + vec![ConnectOutcome::Success(loopback_stream())], + vec![PeerOutcome::Address(wrong_peer)], + ); + let error = plan(MAX_CONNECTION_ATTEMPTS) + .connect_with(&connector) + .expect_err("peer mismatch must fail closed"); + assert!(matches!( + error, + WebDriverBiDiTcpConnectionError::PeerMismatch { + attempt_number: 1, + .. + } + )); + assert_eq!(connector.connect_calls.get(), 1); + assert_eq!(connector.peer_calls.get(), 1); + assert!(error.source().is_some()); +} + +#[test] +fn error_display_source_and_attempt_contracts_cover_every_variant() { + let mismatch = connect_target(false) + .verify_connected_peer(SocketAddr::from(([127, 0, 0, 1], 9516))) + .expect_err("wrong peer must fail"); + let errors = [ + WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { + connect_timeout: Duration::ZERO, + maximum_timeout: MAX_CONNECT_TIMEOUT, + }, + WebDriverBiDiTcpConnectionError::InvalidAttemptCount { + attempt_count: 0, + maximum_attempts: MAX_CONNECTION_ATTEMPTS, + }, + WebDriverBiDiTcpConnectionError::ConnectionTimedOut { + socket_address: socket_address(), + attempt_count: 2, + connect_timeout: Duration::from_millis(250), + source: io::Error::from(io::ErrorKind::TimedOut), + }, + WebDriverBiDiTcpConnectionError::ConnectionFailed { + socket_address: socket_address(), + attempt_count: 3, + source: io::Error::from(io::ErrorKind::ConnectionRefused), + }, + WebDriverBiDiTcpConnectionError::PeerInspectionFailed { + socket_address: socket_address(), + attempt_number: 1, + source: io::Error::from(io::ErrorKind::NotConnected), + }, + WebDriverBiDiTcpConnectionError::PeerMismatch { + attempt_number: 1, + source: mismatch, + }, + ]; + + let messages: Vec = errors.iter().map(ToString::to_string).collect(); + assert!(messages[0].contains("outside 1ns")); + assert!(messages[1].contains("attempt count 0")); + assert!(messages[2].contains("timed out after 2 attempts")); + assert!(messages[3].contains("failed after 3 attempts")); + assert!(messages[4].contains("peer inspection failed")); + assert!(messages[5].contains("did not match the approved target")); + + assert_eq!(errors[0].attempt_count(), None); + assert_eq!(errors[1].attempt_count(), None); + assert_eq!(errors[2].attempt_count(), Some(2)); + assert_eq!(errors[3].attempt_count(), Some(3)); + assert_eq!(errors[4].attempt_count(), Some(1)); + assert_eq!(errors[5].attempt_count(), Some(1)); + assert!(errors[0].source().is_none()); + assert!(errors[1].source().is_none()); + assert!(errors[2].source().is_some()); + assert!(errors[3].source().is_some()); + assert!(errors[4].source().is_some()); + assert!(errors[5].source().is_some()); +} From ec66b21bd4bcae0799a744961aa01a74c0ed66be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:41:36 -0700 Subject: [PATCH 102/118] refactor(network): keep BiDi transport boundary focused --- .../src/webdriver_bidi_connection.rs | 525 +----------------- 1 file changed, 20 insertions(+), 505 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs index 0be687193..edf750d7a 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -1,12 +1,20 @@ -use std::{fmt, io, net::SocketAddr, net::TcpStream, time::Duration}; - -use originweave_core::{ - VerifiedWebDriverBiDiSocketPeer, WebDriverBiDiSocketPeerVerificationError, - WebDriverBiDiWebSocketConnectTarget, +use std::{ + io, + net::{SocketAddr, TcpStream}, + time::Duration, }; +use originweave_core::{VerifiedWebDriverBiDiSocketPeer, WebDriverBiDiWebSocketConnectTarget}; + use crate::connection::{MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS}; +mod error; + +pub use error::WebDriverBiDiTcpConnectionError; + +#[cfg(test)] +mod tests; + fn is_retryable_connect_error(kind: io::ErrorKind) -> bool { matches!( kind, @@ -92,12 +100,13 @@ impl WebDriverBiDiTcpConnectionPlan { source, } })?; - let verified_peer = target.verify_connected_peer(observed_peer).map_err( - |source| WebDriverBiDiTcpConnectionError::PeerMismatch { - attempt_number, - source, - }, - )?; + let verified_peer = + target + .verify_connected_peer(observed_peer) + .map_err(|source| WebDriverBiDiTcpConnectionError::PeerMismatch { + attempt_number, + source, + })?; return Ok(WebDriverBiDiTcpConnection { stream, verified_peer, @@ -195,497 +204,3 @@ impl WebDriverBiDiTcpConnection { self.connect_timeout } } - -/// Deterministic failures while establishing one bounded WebDriver BiDi TCP transport. -#[derive(Debug)] -pub enum WebDriverBiDiTcpConnectionError { - /// The requested timeout was zero or exceeded [`MAX_CONNECT_TIMEOUT`]. - InvalidConnectTimeout { - /// The rejected timeout. - connect_timeout: Duration, - /// The largest accepted per-attempt timeout. - maximum_timeout: Duration, - }, - /// The requested attempt count was outside `1..=MAX_CONNECTION_ATTEMPTS`. - InvalidAttemptCount { - /// The rejected attempt count. - attempt_count: u8, - /// The largest accepted attempt count. - maximum_attempts: u8, - }, - /// The final bounded connection attempt timed out. - ConnectionTimedOut { - /// Exact approved socket address submitted to the operating system. - socket_address: SocketAddr, - /// Number of attempts completed before failure. - attempt_count: u8, - /// Per-attempt timeout used by the plan. - connect_timeout: Duration, - /// Final operating-system timeout error. - source: io::Error, - }, - /// The final bounded connection attempt failed without a timeout. - ConnectionFailed { - /// Exact approved socket address submitted to the operating system. - socket_address: SocketAddr, - /// Number of attempts completed before failure. - attempt_count: u8, - /// Final operating-system connection error. - source: io::Error, - }, - /// The established stream did not reveal an operating-system peer address. - PeerInspectionFailed { - /// Exact approved socket address submitted to the operating system. - socket_address: SocketAddr, - /// One-based attempt that established the stream. - attempt_number: u8, - /// Operating-system peer-inspection error. - source: io::Error, - }, - /// The established stream reported a peer other than the exact approved BiDi target. - PeerMismatch { - /// One-based attempt that established the stream. - attempt_number: u8, - /// Typed core peer-verification failure preserving expected and actual socket addresses. - source: WebDriverBiDiSocketPeerVerificationError, - }, -} - -impl WebDriverBiDiTcpConnectionError { - /// Return the number of transport attempts associated with this failure, when applicable. - #[must_use] - pub const fn attempt_count(&self) -> Option { - match self { - Self::ConnectionTimedOut { attempt_count, .. } - | Self::ConnectionFailed { attempt_count, .. } => Some(*attempt_count), - Self::PeerInspectionFailed { attempt_number, .. } - | Self::PeerMismatch { attempt_number, .. } => Some(*attempt_number), - Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, - } - } -} - -impl fmt::Display for WebDriverBiDiTcpConnectionError { - fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - Self::InvalidConnectTimeout { - connect_timeout, - maximum_timeout, - } => write!( - formatter, - "WebDriver BiDi connect timeout {connect_timeout:?} is outside 1ns..={maximum_timeout:?}", - ), - Self::InvalidAttemptCount { - attempt_count, - maximum_attempts, - } => write!( - formatter, - "WebDriver BiDi connection attempt count {attempt_count} is outside 1..={maximum_attempts}", - ), - Self::ConnectionTimedOut { - socket_address, - attempt_count, - connect_timeout, - .. - } => write!( - formatter, - "WebDriver BiDi TCP connection to {socket_address} timed out after {attempt_count} attempts with per-attempt timeout {connect_timeout:?}", - ), - Self::ConnectionFailed { - socket_address, - attempt_count, - .. - } => write!( - formatter, - "WebDriver BiDi TCP connection to {socket_address} failed after {attempt_count} attempts", - ), - Self::PeerInspectionFailed { - socket_address, - attempt_number, - .. - } => write!( - formatter, - "WebDriver BiDi TCP peer inspection failed for {socket_address} on attempt {attempt_number}", - ), - Self::PeerMismatch { attempt_number, .. } => write!( - formatter, - "WebDriver BiDi TCP peer did not match the approved target on attempt {attempt_number}", - ), - } - } -} - -impl std::error::Error for WebDriverBiDiTcpConnectionError { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - match self { - Self::ConnectionTimedOut { source, .. } - | Self::ConnectionFailed { source, .. } - | Self::PeerInspectionFailed { source, .. } => Some(source), - Self::PeerMismatch { source, .. } => Some(source), - Self::InvalidConnectTimeout { .. } | Self::InvalidAttemptCount { .. } => None, - } - } -} - -#[cfg(test)] -mod tests { - #![allow(clippy::expect_used)] - - use std::{ - cell::{Cell, RefCell}, - collections::VecDeque, - error::Error, - net::{TcpListener, TcpStream}, - }; - - use originweave_core::WebDriverBiDiWebSocketEndpoint; - - use super::*; - - const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; - const SOCKET_ADDRESS: SocketAddr = SocketAddr::from(([127, 0, 0, 1], 9515)); - - enum ConnectOutcome { - Success(TcpStream), - Error(io::ErrorKind), - } - - enum PeerOutcome { - Address(SocketAddr), - Error(io::ErrorKind), - } - - struct FakeConnector { - connect_outcomes: RefCell>, - peer_outcomes: RefCell>, - connect_calls: Cell, - peer_calls: Cell, - } - - impl FakeConnector { - fn new(connect_outcomes: Vec, peer_outcomes: Vec) -> Self { - Self { - connect_outcomes: RefCell::new(connect_outcomes.into()), - peer_outcomes: RefCell::new(peer_outcomes.into()), - connect_calls: Cell::new(0), - peer_calls: Cell::new(0), - } - } - } - - impl WebDriverBiDiSocketConnector for FakeConnector { - fn connect_timeout( - &self, - _socket_address: &SocketAddr, - _timeout: Duration, - ) -> io::Result { - self.connect_calls.set(self.connect_calls.get() + 1); - match self - .connect_outcomes - .borrow_mut() - .pop_front() - .expect("test must provide a connection outcome") - { - ConnectOutcome::Success(stream) => Ok(stream), - ConnectOutcome::Error(kind) => Err(io::Error::from(kind)), - } - } - - fn peer_addr(&self, _stream: &TcpStream) -> io::Result { - self.peer_calls.set(self.peer_calls.get() + 1); - match self - .peer_outcomes - .borrow_mut() - .pop_front() - .expect("test must provide a peer outcome") - { - PeerOutcome::Address(address) => Ok(address), - PeerOutcome::Error(kind) => Err(io::Error::from(kind)), - } - } - } - - fn loopback_stream() -> TcpStream { - let listener = TcpListener::bind(("127.0.0.1", 0)).expect("bind loopback listener"); - let address = listener.local_addr().expect("read loopback listener address"); - let client = TcpStream::connect(address).expect("connect loopback client"); - let (server, _) = listener.accept().expect("accept loopback client"); - drop(server); - client - } - - fn connect_target(secure: bool) -> WebDriverBiDiWebSocketConnectTarget { - let scheme = if secure { "wss" } else { "ws" }; - let endpoint = format!("{scheme}://127.0.0.1:9515/session/{SESSION_ID}"); - let admitted = WebDriverBiDiWebSocketEndpoint::new(&endpoint).expect("admit endpoint"); - let correlated = admitted - .correlate_session_id(SESSION_ID) - .expect("correlate endpoint"); - correlated - .into_explicit_connect_target() - .expect("derive explicit connect target") - } - - fn plan(maximum_attempts: u8) -> WebDriverBiDiTcpConnectionPlan { - WebDriverBiDiTcpConnectionPlan::new( - connect_target(false), - Duration::from_millis(250), - maximum_attempts, - ) - .expect("valid test plan") - } - - #[test] - fn validates_timeout_and_attempt_bounds_before_io() { - let zero_timeout = WebDriverBiDiTcpConnectionPlan::new( - connect_target(false), - Duration::ZERO, - 1, - ); - assert!(matches!( - zero_timeout, - Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { .. }) - )); - - let excessive_timeout = WebDriverBiDiTcpConnectionPlan::new( - connect_target(false), - MAX_CONNECT_TIMEOUT + Duration::from_nanos(1), - 1, - ); - assert!(matches!( - excessive_timeout, - Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { .. }) - )); - - let zero_attempts = WebDriverBiDiTcpConnectionPlan::new( - connect_target(false), - Duration::from_millis(250), - 0, - ); - assert!(matches!( - zero_attempts, - Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { .. }) - )); - - let excessive_attempts = WebDriverBiDiTcpConnectionPlan::new( - connect_target(false), - Duration::from_millis(250), - MAX_CONNECTION_ATTEMPTS + 1, - ); - assert!(matches!( - excessive_attempts, - Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { .. }) - )); - } - - #[test] - fn verified_peer_is_required_before_stream_exposure() { - let connector = FakeConnector::new( - vec![ConnectOutcome::Success(loopback_stream())], - vec![PeerOutcome::Address(SOCKET_ADDRESS)], - ); - let connection = WebDriverBiDiTcpConnectionPlan::new( - connect_target(true), - Duration::from_millis(250), - 1, - ) - .expect("valid plan") - .connect_with(&connector) - .expect("verified connection"); - - assert!(connection.stream().peer_addr().is_ok()); - assert_eq!(connection.verified_peer().socket_addr(), SOCKET_ADDRESS); - assert!(connection.verified_peer().requires_tls()); - assert_eq!(connection.verified_peer().session_id(), SESSION_ID); - assert_eq!(connection.attempt_number(), 1); - assert_eq!(connection.connect_timeout(), Duration::from_millis(250)); - assert_eq!(connector.connect_calls.get(), 1); - assert_eq!(connector.peer_calls.get(), 1); - } - - #[test] - fn all_recoverable_connect_kinds_can_retry_once() { - for kind in [ - io::ErrorKind::TimedOut, - io::ErrorKind::ConnectionRefused, - io::ErrorKind::ConnectionReset, - io::ErrorKind::ConnectionAborted, - io::ErrorKind::Interrupted, - ] { - assert!(is_retryable_connect_error(kind)); - let connector = FakeConnector::new( - vec![ - ConnectOutcome::Error(kind), - ConnectOutcome::Success(loopback_stream()), - ], - vec![PeerOutcome::Address(SOCKET_ADDRESS)], - ); - let connection = plan(2) - .connect_with(&connector) - .expect("second bounded attempt succeeds"); - assert_eq!(connection.attempt_number(), 2); - assert_eq!(connector.connect_calls.get(), 2); - assert_eq!(connector.peer_calls.get(), 1); - } - assert!(!is_retryable_connect_error(io::ErrorKind::PermissionDenied)); - } - - #[test] - fn final_timeout_preserves_source_and_attempt_count() { - let connector = FakeConnector::new( - vec![ConnectOutcome::Error(io::ErrorKind::TimedOut)], - Vec::new(), - ); - let error = plan(1) - .connect_with(&connector) - .expect_err("timeout must fail closed"); - assert!(matches!( - error, - WebDriverBiDiTcpConnectionError::ConnectionTimedOut { - attempt_count: 1, - .. - } - )); - assert!(error.source().is_some()); - assert_eq!(error.attempt_count(), Some(1)); - } - - #[test] - fn exhausted_retryable_non_timeout_error_is_connection_failure() { - let connector = FakeConnector::new( - vec![ConnectOutcome::Error(io::ErrorKind::ConnectionRefused)], - Vec::new(), - ); - let error = plan(1) - .connect_with(&connector) - .expect_err("refusal must fail after the bounded final attempt"); - assert!(matches!( - error, - WebDriverBiDiTcpConnectionError::ConnectionFailed { - attempt_count: 1, - .. - } - )); - assert!(error.source().is_some()); - } - - #[test] - fn non_retryable_connection_error_fails_without_retry() { - let connector = FakeConnector::new( - vec![ConnectOutcome::Error(io::ErrorKind::PermissionDenied)], - Vec::new(), - ); - let error = plan(MAX_CONNECTION_ATTEMPTS) - .connect_with(&connector) - .expect_err("permission failure must not retry"); - assert!(matches!( - error, - WebDriverBiDiTcpConnectionError::ConnectionFailed { - attempt_count: 1, - .. - } - )); - assert_eq!(connector.connect_calls.get(), 1); - } - - #[test] - fn peer_inspection_failure_is_not_retried() { - let connector = FakeConnector::new( - vec![ConnectOutcome::Success(loopback_stream())], - vec![PeerOutcome::Error(io::ErrorKind::NotConnected)], - ); - let error = plan(MAX_CONNECTION_ATTEMPTS) - .connect_with(&connector) - .expect_err("peer inspection failure must fail closed"); - assert!(matches!( - error, - WebDriverBiDiTcpConnectionError::PeerInspectionFailed { - attempt_number: 1, - .. - } - )); - assert_eq!(connector.connect_calls.get(), 1); - assert_eq!(connector.peer_calls.get(), 1); - assert!(error.source().is_some()); - } - - #[test] - fn peer_mismatch_is_not_retried_or_converted_to_success() { - let wrong_peer = SocketAddr::from(([127, 0, 0, 1], 9516)); - let connector = FakeConnector::new( - vec![ConnectOutcome::Success(loopback_stream())], - vec![PeerOutcome::Address(wrong_peer)], - ); - let error = plan(MAX_CONNECTION_ATTEMPTS) - .connect_with(&connector) - .expect_err("peer mismatch must fail closed"); - assert!(matches!( - error, - WebDriverBiDiTcpConnectionError::PeerMismatch { - attempt_number: 1, - .. - } - )); - assert_eq!(connector.connect_calls.get(), 1); - assert_eq!(connector.peer_calls.get(), 1); - assert!(error.source().is_some()); - } - - #[test] - fn error_display_source_and_attempt_contracts_cover_every_variant() { - let mismatch = connect_target(false) - .verify_connected_peer(SocketAddr::from(([127, 0, 0, 1], 9516))) - .expect_err("wrong peer must fail"); - let errors = [ - WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { - connect_timeout: Duration::ZERO, - maximum_timeout: MAX_CONNECT_TIMEOUT, - }, - WebDriverBiDiTcpConnectionError::InvalidAttemptCount { - attempt_count: 0, - maximum_attempts: MAX_CONNECTION_ATTEMPTS, - }, - WebDriverBiDiTcpConnectionError::ConnectionTimedOut { - socket_address: SOCKET_ADDRESS, - attempt_count: 2, - connect_timeout: Duration::from_millis(250), - source: io::Error::from(io::ErrorKind::TimedOut), - }, - WebDriverBiDiTcpConnectionError::ConnectionFailed { - socket_address: SOCKET_ADDRESS, - attempt_count: 3, - source: io::Error::from(io::ErrorKind::ConnectionRefused), - }, - WebDriverBiDiTcpConnectionError::PeerInspectionFailed { - socket_address: SOCKET_ADDRESS, - attempt_number: 1, - source: io::Error::from(io::ErrorKind::NotConnected), - }, - WebDriverBiDiTcpConnectionError::PeerMismatch { - attempt_number: 1, - source: mismatch, - }, - ]; - - let messages: Vec = errors.iter().map(ToString::to_string).collect(); - assert!(messages[0].contains("outside 1ns")); - assert!(messages[1].contains("attempt count 0")); - assert!(messages[2].contains("timed out after 2 attempts")); - assert!(messages[3].contains("failed after 3 attempts")); - assert!(messages[4].contains("peer inspection failed")); - assert!(messages[5].contains("did not match the approved target")); - - assert_eq!(errors[0].attempt_count(), None); - assert_eq!(errors[1].attempt_count(), None); - assert_eq!(errors[2].attempt_count(), Some(2)); - assert_eq!(errors[3].attempt_count(), Some(3)); - assert_eq!(errors[4].attempt_count(), Some(1)); - assert_eq!(errors[5].attempt_count(), Some(1)); - assert!(errors[0].source().is_none()); - assert!(errors[1].source().is_none()); - assert!(errors[2].source().is_some()); - assert!(errors[3].source().is_some()); - assert!(errors[4].source().is_some()); - assert!(errors[5].source().is_some()); - } -} From 755d626ff6e2bf390b66a254261b8084d2e299ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:43:19 -0700 Subject: [PATCH 103/118] test(network): apply canonical BiDi resilience formatting --- .../src/webdriver_bidi_connection/tests.rs | 24 +++++++------------ 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs index 4720493f6..e2d287ca1 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/tests.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/tests.rs @@ -12,8 +12,8 @@ use std::{ use originweave_core::{WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketEndpoint}; use super::{ - is_retryable_connect_error, WebDriverBiDiSocketConnector, WebDriverBiDiTcpConnectionError, - WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiSocketConnector, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, + is_retryable_connect_error, }; use crate::connection::{MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS}; @@ -134,11 +134,8 @@ fn validates_timeout_and_attempt_bounds_before_io() { Err(WebDriverBiDiTcpConnectionError::InvalidConnectTimeout { .. }) )); - let zero_attempts = WebDriverBiDiTcpConnectionPlan::new( - connect_target(false), - Duration::from_millis(250), - 0, - ); + let zero_attempts = + WebDriverBiDiTcpConnectionPlan::new(connect_target(false), Duration::from_millis(250), 0); assert!(matches!( zero_attempts, Err(WebDriverBiDiTcpConnectionError::InvalidAttemptCount { .. }) @@ -161,14 +158,11 @@ fn verified_peer_is_required_before_stream_exposure() { vec![ConnectOutcome::Success(loopback_stream())], vec![PeerOutcome::Address(socket_address())], ); - let connection = WebDriverBiDiTcpConnectionPlan::new( - connect_target(true), - Duration::from_millis(250), - 1, - ) - .expect("valid plan") - .connect_with(&connector) - .expect("verified connection"); + let connection = + WebDriverBiDiTcpConnectionPlan::new(connect_target(true), Duration::from_millis(250), 1) + .expect("valid plan") + .connect_with(&connector) + .expect("verified connection"); assert!(connection.stream().peer_addr().is_ok()); assert_eq!(connection.verified_peer().socket_addr(), socket_address()); From fd91248908eb68a0b483ab588b17ec2834c1e47c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:45:49 -0700 Subject: [PATCH 104/118] fix(network): remove unused BiDi error imports --- .../src/webdriver_bidi_connection/error.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_connection/error.rs b/crates/originweave-network/src/webdriver_bidi_connection/error.rs index 613f3101a..226cd1d2b 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection/error.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection/error.rs @@ -2,12 +2,10 @@ use std::{fmt, io, net::SocketAddr, time::Duration}; use originweave_core::WebDriverBiDiSocketPeerVerificationError; -use crate::connection::{MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS}; - /// Deterministic failures while establishing one bounded WebDriver BiDi TCP transport. #[derive(Debug)] pub enum WebDriverBiDiTcpConnectionError { - /// The requested timeout was zero or exceeded [`MAX_CONNECT_TIMEOUT`]. + /// The requested timeout was zero or exceeded [`crate::MAX_CONNECT_TIMEOUT`]. InvalidConnectTimeout { /// The rejected timeout. connect_timeout: Duration, From 975e2e2653306a41ee0b33d2d5030ed9ccffdfa7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 04:49:51 -0700 Subject: [PATCH 105/118] docs(changelog): record bounded BiDi TCP transport --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7ca383831..587f92976 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Bounded WebDriver BiDi loopback TCP transport that consumes one exact no-DNS connect target, retries only explicitly recoverable local transport failures within repository timeout and attempt ceilings, exposes the stream only after operating-system peer inspection and exact peer verification, preserves correlated session/TLS metadata and typed causal errors, and performs no DNS, proxy/PAC, process authentication, TLS, WebSocket, BiDi message, browser-action, or Agent-authority step. - Exact WebDriver BiDi socket-peer verification that consumes an approved no-DNS connect target, requires the observed IP address and port to match exactly, preserves the TLS requirement and exact correlated session id, and remains inert metadata that does not authenticate an OS process, does not negotiate TLS, perform a WebSocket handshake, or grant Agent authority. - Explicit no-DNS WebDriver BiDi loopback connection targets that derive exact IPv4/IPv6 loopback `SocketAddr` metadata from a session-correlated endpoint, reject `localhost` as requiring separately trusted name resolution, preserve the TLS requirement and exact session id, perform no socket I/O, and grant no Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. From c491464e8c83bf45d96cf3f4d9ea01f4b437996c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:00:20 -0700 Subject: [PATCH 106/118] test(network): require consumable BiDi TCP evidence --- .../tests/webdriver_bidi_tcp_connection.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs b/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs index bfc80457b..fac15b6a5 100644 --- a/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs +++ b/crates/originweave-network/tests/webdriver_bidi_tcp_connection.rs @@ -60,6 +60,14 @@ fn exact_loopback_target_opens_one_verified_bidi_tcp_stream() { assert_eq!(connection.attempt_number(), 1); assert_eq!(connection.connect_timeout(), Duration::from_secs(1)); + let (stream, evidence) = connection.into_parts(); + assert_eq!(stream.peer_addr().ok(), Some(local_addr)); + assert_eq!(evidence.verified_peer().socket_addr(), local_addr); + assert!(!evidence.verified_peer().requires_tls()); + assert_eq!(evidence.verified_peer().session_id(), SESSION_ID); + assert_eq!(evidence.attempt_number(), 1); + assert_eq!(evidence.connect_timeout(), Duration::from_secs(1)); + let server_result = server.join(); assert!(server_result.is_ok(), "{server_result:?}"); if let Ok(accept_result) = server_result { From 6aab73ccf742c570067e7c23621c80209b65f1af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:03:38 -0700 Subject: [PATCH 107/118] feat(network): hand off verified BiDi TCP evidence --- crates/originweave-network/src/lib.rs | 3 +- .../src/webdriver_bidi_connection.rs | 48 +++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 67f85c973..3c267b6ed 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -18,5 +18,6 @@ pub use connection::{ NetworkError, SocketConnectionEvidence, }; pub use webdriver_bidi_connection::{ - WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionPlan, + WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, + WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan, }; diff --git a/crates/originweave-network/src/webdriver_bidi_connection.rs b/crates/originweave-network/src/webdriver_bidi_connection.rs index edf750d7a..5d39bb5e3 100644 --- a/crates/originweave-network/src/webdriver_bidi_connection.rs +++ b/crates/originweave-network/src/webdriver_bidi_connection.rs @@ -203,4 +203,52 @@ impl WebDriverBiDiTcpConnection { pub const fn connect_timeout(&self) -> Duration { self.connect_timeout } + + /// Consume the wrapper into the original verified stream and credential-free transport evidence. + /// + /// This handoff does not clone the socket or create reusable connection authority. The returned + /// evidence records only the already-verified peer plus bounded connection-attempt metadata; it + /// does not authenticate a browser process, establish TLS, complete WebSocket framing, or grant + /// browser or Agent authority. + #[must_use] + pub fn into_parts(self) -> (TcpStream, WebDriverBiDiTcpConnectionEvidence) { + let evidence = WebDriverBiDiTcpConnectionEvidence { + verified_peer: self.verified_peer, + attempt_number: self.attempt_number, + connect_timeout: self.connect_timeout, + }; + (self.stream, evidence) + } +} + +/// Credential-free evidence retained when a verified WebDriver BiDi TCP stream is consumed. +/// +/// This value records exact peer/session/TLS-requirement metadata inherited from the consumed +/// no-DNS target together with the successful bounded attempt and per-attempt timeout. It is +/// transport evidence only and grants no process, TLS, WebSocket, browser-action, or Agent authority. +#[derive(Debug)] +pub struct WebDriverBiDiTcpConnectionEvidence { + verified_peer: VerifiedWebDriverBiDiSocketPeer, + attempt_number: u8, + connect_timeout: Duration, +} + +impl WebDriverBiDiTcpConnectionEvidence { + /// Borrow the exact session-correlated peer verified before stream exposure. + #[must_use] + pub const fn verified_peer(&self) -> &VerifiedWebDriverBiDiSocketPeer { + &self.verified_peer + } + + /// Return the one-based bounded attempt on which the connection succeeded. + #[must_use] + pub const fn attempt_number(&self) -> u8 { + self.attempt_number + } + + /// Return the per-attempt timeout applied while establishing the connection. + #[must_use] + pub const fn connect_timeout(&self) -> Duration { + self.connect_timeout + } } From 18703be8dc09d875834761f937165232f73e6c2d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:07:23 -0700 Subject: [PATCH 108/118] docs(changelog): record BiDi TCP evidence handoff --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 587f92976..5598829e7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,7 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added -- Bounded WebDriver BiDi loopback TCP transport that consumes one exact no-DNS connect target, retries only explicitly recoverable local transport failures within repository timeout and attempt ceilings, exposes the stream only after operating-system peer inspection and exact peer verification, preserves correlated session/TLS metadata and typed causal errors, and performs no DNS, proxy/PAC, process authentication, TLS, WebSocket, BiDi message, browser-action, or Agent-authority step. +- Bounded WebDriver BiDi loopback TCP transport that consumes one exact no-DNS connect target, retries only explicitly recoverable local transport failures within repository timeout and attempt ceilings, exposes the stream only after operating-system peer inspection and exact peer verification, supports a consuming handoff of the original stream with typed credential-free peer/session/TLS and bounded-attempt evidence, preserves typed causal errors, and performs no DNS, proxy/PAC, process authentication, TLS, WebSocket, BiDi message, browser-action, or Agent-authority step. - Exact WebDriver BiDi socket-peer verification that consumes an approved no-DNS connect target, requires the observed IP address and port to match exactly, preserves the TLS requirement and exact correlated session id, and remains inert metadata that does not authenticate an OS process, does not negotiate TLS, perform a WebSocket handshake, or grant Agent authority. - Explicit no-DNS WebDriver BiDi loopback connection targets that derive exact IPv4/IPv6 loopback `SocketAddr` metadata from a session-correlated endpoint, reject `localhost` as requiring separately trusted name resolution, preserve the TLS requirement and exact session id, perform no socket I/O, and grant no Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. From 11a533dfba366f0b11b36c47a2470ad57fd0344a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:14:35 -0700 Subject: [PATCH 109/118] test(network): require bounded BiDi WebSocket opening request --- .../webdriver_bidi_websocket_handshake.rs | 126 ++++++++++++++++++ 1 file changed, 126 insertions(+) create mode 100644 crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs new file mode 100644 index 000000000..33a632bb0 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs @@ -0,0 +1,126 @@ +use std::{net::TcpListener, thread, time::Duration}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakeError, WebDriverBiDiWebSocketHandshakePlan, +}; + +const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; + +fn connect(endpoint: &str) -> originweave_network::WebDriverBiDiTcpConnection { + let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); + assert!(admitted.is_ok(), "{admitted:?}"); + let Ok(admitted) = admitted else { + unreachable!("asserted valid endpoint") + }; + let correlated = admitted.correlate_session_id(SESSION_ID); + assert!(correlated.is_ok(), "{correlated:?}"); + let Ok(correlated) = correlated else { + unreachable!("asserted correlated endpoint") + }; + let target = correlated.into_explicit_connect_target(); + assert!(target.is_ok(), "{target:?}"); + let Ok(target) = target else { + unreachable!("asserted explicit target") + }; + let plan = WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1); + assert!(plan.is_ok(), "{plan:?}"); + let Ok(plan) = plan else { + unreachable!("asserted connection plan") + }; + let connection = plan.connect(); + assert!(connection.is_ok(), "{connection:?}"); + let Ok(connection) = connection else { + unreachable!("asserted loopback connection") + }; + connection +} + +#[test] +fn plain_bidi_connection_serializes_exact_rfc6455_opening_request() { + let listener = TcpListener::bind(("127.0.0.1", 0)); + assert!(listener.is_ok(), "{listener:?}"); + let Ok(listener) = listener else { + return; + }; + let local_addr = listener.local_addr(); + assert!(local_addr.is_ok(), "{local_addr:?}"); + let Ok(local_addr) = local_addr else { + return; + }; + let server = thread::spawn(move || listener.accept().map(|_| ())); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let connection = connect(&endpoint); + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY); + assert!(key.is_ok(), "{key:?}"); + let Ok(key) = key else { + return; + }; + let plan = WebDriverBiDiWebSocketHandshakePlan::new(connection, key); + assert!(plan.is_ok(), "{plan:?}"); + let Ok(plan) = plan else { + return; + }; + + let expected = format!( + "GET /session/{SESSION_ID} HTTP/1.1\r\nHost: {local_addr}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {RFC6455_SAMPLE_KEY}\r\nSec-WebSocket-Version: 13\r\n\r\n" + ); + assert_eq!(plan.request_bytes(), expected.as_bytes()); + assert_eq!(plan.verified_peer().socket_addr(), local_addr); + assert_eq!(plan.verified_peer().session_id(), SESSION_ID); + assert!(!plan.verified_peer().requires_tls()); + + let server_result = server.join(); + assert!(server_result.is_ok(), "{server_result:?}"); + if let Ok(accept_result) = server_result { + assert!(accept_result.is_ok(), "{accept_result:?}"); + } +} + +#[test] +fn handshake_plan_rejects_tls_required_stream_and_noncanonical_client_keys() { + let invalid_character = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZ!=="); + assert!(matches!( + invalid_character, + Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey) + )); + let invalid_padding_bits = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZR=="); + assert!(matches!( + invalid_padding_bits, + Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey) + )); + + let listener = TcpListener::bind(("127.0.0.1", 0)); + assert!(listener.is_ok(), "{listener:?}"); + let Ok(listener) = listener else { + return; + }; + let local_addr = listener.local_addr(); + assert!(local_addr.is_ok(), "{local_addr:?}"); + let Ok(local_addr) = local_addr else { + return; + }; + let server = thread::spawn(move || listener.accept().map(|_| ())); + + let endpoint = format!("wss://{local_addr}/session/{SESSION_ID}"); + let connection = connect(&endpoint); + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY); + assert!(key.is_ok(), "{key:?}"); + let Ok(key) = key else { + return; + }; + let plan = WebDriverBiDiWebSocketHandshakePlan::new(connection, key); + assert!(matches!( + plan, + Err(WebDriverBiDiWebSocketHandshakeError::TlsRequired) + )); + + let server_result = server.join(); + assert!(server_result.is_ok(), "{server_result:?}"); + if let Ok(accept_result) = server_result { + assert!(accept_result.is_ok(), "{accept_result:?}"); + } +} From 980330348d8007000a5c8ed4b049390a97e591e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 05:18:13 -0700 Subject: [PATCH 110/118] feat(network): bind BiDi WebSocket opening request --- crates/originweave-network/src/lib.rs | 10 +- .../src/webdriver_bidi_websocket_handshake.rs | 124 ++++++++++++++++++ 2 files changed, 132 insertions(+), 2 deletions(-) create mode 100644 crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index 3c267b6ed..a77d9b794 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -4,14 +4,16 @@ //! without hostname resolution or proxy inheritance, verifies operating-system //! peers before exposing transport I/O, and emits credential-free evidence. //! It also bridges a session-correlated WebDriver BiDi loopback target from -//! `originweave-core` into one bounded exact TCP connection without granting -//! browser, WebSocket, TLS, policy, or Agent authority. +//! `originweave-core` into one bounded exact TCP connection and can bind an inert +//! RFC 6455 opening request to an already-verified plain BiDi stream without +//! granting browser, WebSocket, TLS, policy, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] mod connection; mod webdriver_bidi_connection; +mod webdriver_bidi_websocket_handshake; pub use connection::{ ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS, @@ -21,3 +23,7 @@ pub use webdriver_bidi_connection::{ WebDriverBiDiTcpConnection, WebDriverBiDiTcpConnectionError, WebDriverBiDiTcpConnectionEvidence, WebDriverBiDiTcpConnectionPlan, }; +pub use webdriver_bidi_websocket_handshake::{ + WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakeError, + WebDriverBiDiWebSocketHandshakePlan, +}; diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs new file mode 100644 index 000000000..8b0854305 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs @@ -0,0 +1,124 @@ +use std::fmt; + +use originweave_core::VerifiedWebDriverBiDiSocketPeer; + +use crate::WebDriverBiDiTcpConnection; + +const WEBSOCKET_CLIENT_KEY_LENGTH: usize = 24; + +fn is_base64_data_byte(byte: u8) -> bool { + byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/') +} + +fn is_canonical_16_byte_base64(value: &str) -> bool { + let bytes = value.as_bytes(); + bytes.len() == WEBSOCKET_CLIENT_KEY_LENGTH + && bytes[..22].iter().copied().all(is_base64_data_byte) + && matches!(bytes[21], b'A' | b'Q' | b'g' | b'w') + && bytes[22] == b'=' + && bytes[23] == b'=' +} + +/// Deterministic failures while preparing one WebDriver BiDi RFC 6455 opening request. +#[derive(Debug, Eq, PartialEq)] +pub enum WebDriverBiDiWebSocketHandshakeError { + /// The supplied client key was not the canonical base64 representation of exactly 16 bytes. + InvalidClientKey, + /// The verified WebDriver BiDi target requires TLS before a WebSocket opening request is sent. + TlsRequired, +} + +impl fmt::Display for WebDriverBiDiWebSocketHandshakeError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidClientKey => formatter.write_str( + "WebDriver BiDi WebSocket client key is not canonical base64 for exactly 16 bytes", + ), + Self::TlsRequired => formatter.write_str( + "WebDriver BiDi WebSocket target requires authenticated TLS before the opening request", + ), + } + } +} + +impl std::error::Error for WebDriverBiDiWebSocketHandshakeError {} + +/// Canonical RFC 6455 client key for one WebDriver BiDi opening handshake. +/// +/// RFC 6455 requires `Sec-WebSocket-Key` to be a nonce of 16 bytes encoded with base64. This type +/// validates only the canonical wire representation, including zero padding bits. It does not +/// generate entropy: callers remain responsible for supplying a fresh, unpredictable 16-byte nonce +/// for each connection attempt. +#[derive(Debug, Eq, PartialEq)] +pub struct WebDriverBiDiWebSocketClientKey(String); + +impl WebDriverBiDiWebSocketClientKey { + /// Admit one canonical base64 client key representing exactly 16 bytes. + pub fn new(value: &str) -> Result { + if !is_canonical_16_byte_base64(value) { + return Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey); + } + Ok(Self(value.to_owned())) + } + + /// Borrow the exact canonical value for `Sec-WebSocket-Key` serialization. + #[must_use] + pub fn as_str(&self) -> &str { + &self.0 + } +} + +/// Inert RFC 6455 opening request bound to one already-verified plain BiDi TCP connection. +/// +/// The plan consumes the verified TCP connection so the opening request cannot be detached from the +/// socket peer/session evidence that authorized its exact loopback destination. It serializes only +/// the fixed WebSocket version-13 request required for the admitted `/session/` resource. +/// Secure `wss` targets fail closed here and require a separate authenticated TLS transport boundary +/// before any WebSocket bytes may be written. +/// +/// Construction performs no socket write, TLS operation, response parsing, `Sec-WebSocket-Accept` +/// validation, WebSocket framing, Chromium/ChromeDriver process authentication, browser action, or +/// Agent-authority grant. +#[derive(Debug)] +pub struct WebDriverBiDiWebSocketHandshakePlan { + connection: WebDriverBiDiTcpConnection, + request: Vec, +} + +impl WebDriverBiDiWebSocketHandshakePlan { + /// Bind one canonical opening request to an already-verified plain BiDi TCP connection. + pub fn new( + connection: WebDriverBiDiTcpConnection, + client_key: WebDriverBiDiWebSocketClientKey, + ) -> Result { + if connection.verified_peer().requires_tls() { + return Err(WebDriverBiDiWebSocketHandshakeError::TlsRequired); + } + + let peer = connection.verified_peer(); + let request = format!( + "GET /session/{} HTTP/1.1\r\nHost: {}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {}\r\nSec-WebSocket-Version: 13\r\n\r\n", + peer.session_id(), + peer.socket_addr(), + client_key.as_str(), + ) + .into_bytes(); + + Ok(Self { + connection, + request, + }) + } + + /// Borrow the exact serialized RFC 6455 opening-request bytes. + #[must_use] + pub fn request_bytes(&self) -> &[u8] { + &self.request + } + + /// Borrow the exact peer/session evidence already verified before request construction. + #[must_use] + pub const fn verified_peer(&self) -> &VerifiedWebDriverBiDiSocketPeer { + self.connection.verified_peer() + } +} From 8b8f664513b863d33c31452ae6b49a647f693a07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:05:32 -0700 Subject: [PATCH 111/118] test(network): cover BiDi handshake diagnostics --- .../tests/webdriver_bidi_websocket_handshake.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs index 33a632bb0..c45fc6315 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs @@ -80,6 +80,18 @@ fn plain_bidi_connection_serializes_exact_rfc6455_opening_request() { } } +#[test] +fn handshake_errors_render_actionable_fail_closed_messages() { + assert_eq!( + WebDriverBiDiWebSocketHandshakeError::InvalidClientKey.to_string(), + "WebDriver BiDi WebSocket client key is not canonical base64 for exactly 16 bytes" + ); + assert_eq!( + WebDriverBiDiWebSocketHandshakeError::TlsRequired.to_string(), + "WebDriver BiDi WebSocket target requires authenticated TLS before the opening request" + ); +} + #[test] fn handshake_plan_rejects_tls_required_stream_and_noncanonical_client_keys() { let invalid_character = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZ!=="); From 73f2fbdc1cbb1a26e5d80654b2afb816fa0c4d6a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:11:11 -0700 Subject: [PATCH 112/118] test(network): close BiDi handshake branch coverage --- .../tests/webdriver_bidi_websocket_handshake.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs index c45fc6315..987fcfd6c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs @@ -94,6 +94,11 @@ fn handshake_errors_render_actionable_fail_closed_messages() { #[test] fn handshake_plan_rejects_tls_required_stream_and_noncanonical_client_keys() { + let invalid_length = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQ="); + assert!(matches!( + invalid_length, + Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey) + )); let invalid_character = WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZ!=="); assert!(matches!( invalid_character, @@ -104,6 +109,12 @@ fn handshake_plan_rejects_tls_required_stream_and_noncanonical_client_keys() { invalid_padding_bits, Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey) )); + let invalid_padding_character = + WebDriverBiDiWebSocketClientKey::new("dGhlIHNhbXBsZSBub25jZQA="); + assert!(matches!( + invalid_padding_character, + Err(WebDriverBiDiWebSocketHandshakeError::InvalidClientKey) + )); let listener = TcpListener::bind(("127.0.0.1", 0)); assert!(listener.is_ok(), "{listener:?}"); From d39d0d36508934ff06781ac0dca7a360b6b3d0d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:18:37 -0700 Subject: [PATCH 113/118] test(network): retain BiDi WebSocket client key --- .../tests/webdriver_bidi_websocket_handshake.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs index 987fcfd6c..264f5a46c 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs @@ -69,6 +69,7 @@ fn plain_bidi_connection_serializes_exact_rfc6455_opening_request() { "GET /session/{SESSION_ID} HTTP/1.1\r\nHost: {local_addr}\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Key: {RFC6455_SAMPLE_KEY}\r\nSec-WebSocket-Version: 13\r\n\r\n" ); assert_eq!(plan.request_bytes(), expected.as_bytes()); + assert_eq!(plan.client_key().as_str(), RFC6455_SAMPLE_KEY); assert_eq!(plan.verified_peer().socket_addr(), local_addr); assert_eq!(plan.verified_peer().session_id(), SESSION_ID); assert!(!plan.verified_peer().requires_tls()); From ccfc641e50fe4c381c13d2eae3857455eb9ed0b1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:20:21 -0700 Subject: [PATCH 114/118] fix(network): retain BiDi WebSocket client key --- .../src/webdriver_bidi_websocket_handshake.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs index 8b0854305..6e896800d 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs @@ -72,7 +72,8 @@ impl WebDriverBiDiWebSocketClientKey { /// /// The plan consumes the verified TCP connection so the opening request cannot be detached from the /// socket peer/session evidence that authorized its exact loopback destination. It serializes only -/// the fixed WebSocket version-13 request required for the admitted `/session/` resource. +/// the fixed WebSocket version-13 request required for the admitted `/session/` resource +/// and retains the exact client key required to validate a later `Sec-WebSocket-Accept` response. /// Secure `wss` targets fail closed here and require a separate authenticated TLS transport boundary /// before any WebSocket bytes may be written. /// @@ -82,6 +83,7 @@ impl WebDriverBiDiWebSocketClientKey { #[derive(Debug)] pub struct WebDriverBiDiWebSocketHandshakePlan { connection: WebDriverBiDiTcpConnection, + client_key: WebDriverBiDiWebSocketClientKey, request: Vec, } @@ -106,6 +108,7 @@ impl WebDriverBiDiWebSocketHandshakePlan { Ok(Self { connection, + client_key, request, }) } @@ -116,6 +119,12 @@ impl WebDriverBiDiWebSocketHandshakePlan { &self.request } + /// Borrow the exact client key that a later server-handshake validator must correlate. + #[must_use] + pub const fn client_key(&self) -> &WebDriverBiDiWebSocketClientKey { + &self.client_key + } + /// Borrow the exact peer/session evidence already verified before request construction. #[must_use] pub const fn verified_peer(&self) -> &VerifiedWebDriverBiDiSocketPeer { From 2495b5a6db3643d2fb030aec410fdedae0b950b8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:41:27 -0700 Subject: [PATCH 115/118] test(core): cover ChromeDriver session id compatibility --- .../tests/webdriver_bidi_websocket_endpoint.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index ecf5df6b6..1e38f8ec4 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -6,6 +6,7 @@ use originweave_core::{ }; const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; +const CHROMEDRIVER_SESSION_ID: &str = "0123456789abcdef0123456789abcdef"; #[test] fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority() { @@ -43,6 +44,18 @@ fn canonical_loopback_session_endpoints_are_admitted_without_granting_authority( assert_eq!(ipv6.port(), 9222); } +#[test] +fn chromedriver_generated_session_identifier_is_admitted_for_real_chromium_fixture() { + let endpoint = WebDriverBiDiWebSocketEndpoint::new(&format!( + "ws://127.0.0.1:9515/session/{CHROMEDRIVER_SESSION_ID}" + )); + assert!(endpoint.is_ok(), "{endpoint:?}"); + let Ok(endpoint) = endpoint else { + return; + }; + assert_eq!(endpoint.session_id(), CHROMEDRIVER_SESSION_ID); +} + #[test] fn remote_or_ambiguous_authorities_fail_closed() { for endpoint in [ From baf917c07cdf7187c66a3bda721970e1bc0b3657 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 07:44:51 -0700 Subject: [PATCH 116/118] fix(core): accept canonical ChromeDriver session ids --- .../src/webdriver_bidi_websocket_endpoint.rs | 39 +++++++++++-------- .../webdriver_bidi_websocket_endpoint.rs | 4 ++ 2 files changed, 27 insertions(+), 16 deletions(-) diff --git a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs index 3a555493b..68ba9b061 100644 --- a/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/src/webdriver_bidi_websocket_endpoint.rs @@ -115,7 +115,7 @@ impl WebDriverBiDiWebSocketEndpoint { if session_id.is_empty() || session_id.contains('/') { return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionResource); } - if !is_canonical_session_uuid(session_id) { + if !is_canonical_session_id(session_id) { return Err(WebDriverBiDiWebSocketEndpointAdmissionError::InvalidSessionId); } @@ -152,29 +152,36 @@ impl WebDriverBiDiWebSocketEndpoint { self.port } - /// Return the canonical lower-case UUID session identifier. + /// Return the exact canonical session identifier admitted from the WebDriver endpoint. #[must_use] pub fn session_id(&self) -> &str { &self.session_id } } -fn is_canonical_session_uuid(value: &str) -> bool { +fn is_lowercase_hex(byte: u8) -> bool { + byte.is_ascii_digit() || matches!(byte, b'a'..=b'f') +} + +fn is_canonical_session_id(value: &str) -> bool { let bytes = value.as_bytes(); - if bytes.len() != 36 { - return false; - } - for (index, byte) in bytes.iter().copied().enumerate() { - let valid = if matches!(index, 8 | 13 | 18 | 23) { - byte == b'-' - } else { - byte.is_ascii_digit() || matches!(byte, b'a'..=b'f') - }; - if !valid { - return false; + match bytes.len() { + 32 => bytes.iter().copied().all(is_lowercase_hex), + 36 => { + for (index, byte) in bytes.iter().copied().enumerate() { + let valid = if matches!(index, 8 | 13 | 18 | 23) { + byte == b'-' + } else { + is_lowercase_hex(byte) + }; + if !valid { + return false; + } + } + true } + _ => false, } - true } /// Fail-closed admission errors for WebDriver BiDi WebSocket endpoint metadata. @@ -198,7 +205,7 @@ pub enum WebDriverBiDiWebSocketEndpointAdmissionError { InvalidPort, /// The path is not exactly one `/session/` resource. InvalidSessionResource, - /// The session id is not one canonical lower-case UUID representation. + /// The session id is not an admitted canonical W3C/ChromeDriver representation. InvalidSessionId, } diff --git a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs index 1e38f8ec4..9440dff76 100644 --- a/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs +++ b/crates/originweave-core/tests/webdriver_bidi_websocket_endpoint.rs @@ -132,6 +132,10 @@ fn port_and_session_resource_are_canonical_and_bounded() { "0123456789ab-cdef-0123-456789abcdef", "01234567-89ab-cdef-0123-456789abcdeg", "01234567_89ab-cdef-0123-456789abcdef", + "0123456789abcdef0123456789abcdeF", + "0123456789abcdef0123456789abcdeg", + "0123456789abcdef0123456789abcde_", + "0123456789abcdef0123456789abcde", ] { assert!(matches!( WebDriverBiDiWebSocketEndpoint::new(&format!( From 30da73a684eacd01c78472dc0120258965480091 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 04:02:25 -0700 Subject: [PATCH 117/118] test(network): redact WebSocket client nonce debug --- .../webdriver_bidi_websocket_handshake.rs | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs index 264f5a46c..93550245a 100644 --- a/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/tests/webdriver_bidi_websocket_handshake.rs @@ -8,6 +8,7 @@ use originweave_network::{ const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef"; const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ=="; +const REDACTED_CLIENT_KEY: &str = ""; fn connect(endpoint: &str) -> originweave_network::WebDriverBiDiTcpConnection { let admitted = WebDriverBiDiWebSocketEndpoint::new(endpoint); @@ -38,6 +39,57 @@ fn connect(endpoint: &str) -> originweave_network::WebDriverBiDiTcpConnection { connection } +#[test] +fn client_key_debug_redacts_websocket_nonce() { + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY); + assert!(key.is_ok(), "{key:?}"); + let Ok(key) = key else { + return; + }; + + let debug = format!("{key:?}"); + assert!(debug.contains(REDACTED_CLIENT_KEY)); + assert!(!debug.contains(RFC6455_SAMPLE_KEY)); +} + +#[test] +fn handshake_plan_debug_redacts_websocket_nonce() { + let listener = TcpListener::bind(("127.0.0.1", 0)); + assert!(listener.is_ok(), "{listener:?}"); + let Ok(listener) = listener else { + return; + }; + let local_addr = listener.local_addr(); + assert!(local_addr.is_ok(), "{local_addr:?}"); + let Ok(local_addr) = local_addr else { + return; + }; + let server = thread::spawn(move || listener.accept().map(|_| ())); + + let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}"); + let connection = connect(&endpoint); + let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY); + assert!(key.is_ok(), "{key:?}"); + let Ok(key) = key else { + return; + }; + let plan = WebDriverBiDiWebSocketHandshakePlan::new(connection, key); + assert!(plan.is_ok(), "{plan:?}"); + let Ok(plan) = plan else { + return; + }; + + let debug = format!("{plan:?}"); + assert!(debug.contains(REDACTED_CLIENT_KEY)); + assert!(!debug.contains(RFC6455_SAMPLE_KEY)); + + let server_result = server.join(); + assert!(server_result.is_ok(), "{server_result:?}"); + if let Ok(accept_result) = server_result { + assert!(accept_result.is_ok(), "{accept_result:?}"); + } +} + #[test] fn plain_bidi_connection_serializes_exact_rfc6455_opening_request() { let listener = TcpListener::bind(("127.0.0.1", 0)); From 6922dd98779e8f8aad132a3b1f563d7ba6e6d070 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 22 Aug 2026 04:05:33 -0700 Subject: [PATCH 118/118] fix(network): redact WebSocket client nonce debug --- .../src/webdriver_bidi_websocket_handshake.rs | 30 ++++++++++++++++--- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs b/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs index 6e896800d..026fa390b 100644 --- a/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs +++ b/crates/originweave-network/src/webdriver_bidi_websocket_handshake.rs @@ -5,6 +5,7 @@ use originweave_core::VerifiedWebDriverBiDiSocketPeer; use crate::WebDriverBiDiTcpConnection; const WEBSOCKET_CLIENT_KEY_LENGTH: usize = 24; +const REDACTED_WEBSOCKET_CLIENT_NONCE: &str = ""; fn is_base64_data_byte(byte: u8) -> bool { byte.is_ascii_alphanumeric() || matches!(byte, b'+' | b'/') @@ -48,10 +49,20 @@ impl std::error::Error for WebDriverBiDiWebSocketHandshakeError {} /// RFC 6455 requires `Sec-WebSocket-Key` to be a nonce of 16 bytes encoded with base64. This type /// validates only the canonical wire representation, including zero padding bits. It does not /// generate entropy: callers remain responsible for supplying a fresh, unpredictable 16-byte nonce -/// for each connection attempt. -#[derive(Debug, Eq, PartialEq)] +/// for each connection attempt. Its [`fmt::Debug`] representation deliberately redacts the nonce so +/// diagnostic output cannot disclose handshake material. +#[derive(Eq, PartialEq)] pub struct WebDriverBiDiWebSocketClientKey(String); +impl fmt::Debug for WebDriverBiDiWebSocketClientKey { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_tuple("WebDriverBiDiWebSocketClientKey") + .field(&REDACTED_WEBSOCKET_CLIENT_NONCE) + .finish() + } +} + impl WebDriverBiDiWebSocketClientKey { /// Admit one canonical base64 client key representing exactly 16 bytes. pub fn new(value: &str) -> Result { @@ -75,18 +86,29 @@ impl WebDriverBiDiWebSocketClientKey { /// the fixed WebSocket version-13 request required for the admitted `/session/` resource /// and retains the exact client key required to validate a later `Sec-WebSocket-Accept` response. /// Secure `wss` targets fail closed here and require a separate authenticated TLS transport boundary -/// before any WebSocket bytes may be written. +/// before any WebSocket bytes may be written. Its [`fmt::Debug`] representation omits the serialized +/// request and redacts the client nonce because the request embeds that nonce in `Sec-WebSocket-Key`. /// /// Construction performs no socket write, TLS operation, response parsing, `Sec-WebSocket-Accept` /// validation, WebSocket framing, Chromium/ChromeDriver process authentication, browser action, or /// Agent-authority grant. -#[derive(Debug)] pub struct WebDriverBiDiWebSocketHandshakePlan { connection: WebDriverBiDiTcpConnection, client_key: WebDriverBiDiWebSocketClientKey, request: Vec, } +impl fmt::Debug for WebDriverBiDiWebSocketHandshakePlan { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("WebDriverBiDiWebSocketHandshakePlan") + .field("verified_peer", self.connection.verified_peer()) + .field("client_nonce", &REDACTED_WEBSOCKET_CLIENT_NONCE) + .field("request_byte_count", &self.request.len()) + .finish() + } +} + impl WebDriverBiDiWebSocketHandshakePlan { /// Bind one canonical opening request to an already-verified plain BiDi TCP connection. pub fn new(