diff --git a/CHANGELOG.md b/CHANGELOG.md index ec4d6f8de..ef6693a69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,12 +6,14 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Integrated the current connection-provenance prerequisite into bounded pointer-click serialization, preserving its command validation and inert authority boundary while restoring the inherited executable release contract. - Removed an unused private correlated-response accessor while retaining connection-generation validation at the receiving-message boundary, and corrected the Rust `AtomicU64` standard-library reference to its canonical type-alias page. - Integrated the current teardown prerequisites into transport-closure observation, including the previously uncollected release-record check, while retaining the unresolved connection-provenance finding and its downstream repair ownership. - Integrated the verified opening-exchange and closure prerequisites into the connection-bound response repair, preserving its sender, receiver and closure provenance checks while restoring the inherited executable release contract; process and profile cleanup remain unproven. ### Added +- Deterministic WebDriver BiDi primary-button click serialization for an already admitted remote node: it emits one fixed `input.performActions` mouse sequence from bounded command/context/node identifiers and remains inert until a trusted adapter binds it to current session, origin, document, policy, and approval authority. - Typed outbound WebDriver BiDi `session.end` over the bounded client WebSocket stream: it serializes only the standards-defined method with empty params, rejects invalid frame deadlines before correlation registration, retires only the just-registered id when frame preflight proves no command bytes were emitted, preserves exact command-kind correlation across ambiguous writes, and does not treat frame-write success as proof that the browser session ended. - Typed `session.end` response admission that consumes only the exact outstanding command-kind correlation after complete envelope validation, preserves remote protocol errors as failures, and does not claim browser-process exit or resource cleanup from a protocol acknowledgment. - Fail-closed `session.end` teardown assessment that binds only the typed observation produced by consuming the exact transport, keeps browser-process-exit and task-profile-removal evidence unavailable until their runtime owners exist, and therefore cannot report operational completion from caller-supplied booleans. diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index b9d69c8b0..423654544 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -71,10 +71,11 @@ pub use browser_registry::{ pub use contracts::*; pub use webdriver_bidi_command::{ CorrelatedWebDriverBiDiLocateNodesResponse, MAX_WEBDRIVER_BIDI_COMMAND_ID, - ValidatedWebDriverBiDiLocateNodesResponse, WebDriverBiDiCommandResponseKind, - WebDriverBiDiLocateNodesCommand, WebDriverBiDiLocateNodesCommandError, - WebDriverBiDiLocateNodesResponseCorrelationError, - WebDriverBiDiLocateNodesResponseEnvelopeError, + ValidatedWebDriverBiDiLocateNodesResponse, WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD, + WebDriverBiDiCommandResponseKind, WebDriverBiDiLocateNodesCommand, + WebDriverBiDiLocateNodesCommandError, WebDriverBiDiLocateNodesResponseCorrelationError, + WebDriverBiDiLocateNodesResponseEnvelopeError, WebDriverBiDiPointerClickCommand, + WebDriverBiDiPointerClickCommandError, }; pub use webdriver_bidi_error_code::WebDriverBiDiErrorCode; pub use webdriver_bidi_response_document::{ diff --git a/crates/originweave-core/src/webdriver_bidi_command.rs b/crates/originweave-core/src/webdriver_bidi_command.rs index 9a019cc45..074472348 100644 --- a/crates/originweave-core/src/webdriver_bidi_command.rs +++ b/crates/originweave-core/src/webdriver_bidi_command.rs @@ -10,6 +10,103 @@ use crate::{ /// Maximum WebDriver BiDi command identifier representable by the protocol `js-uint` type. pub const MAX_WEBDRIVER_BIDI_COMMAND_ID: u64 = 9_007_199_254_740_991; +/// WebDriver BiDi method used for one bounded typed pointer action sequence. +pub const WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD: &str = "input.performActions"; + +/// Fail-closed validation errors for one serialized WebDriver BiDi pointer click command. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum WebDriverBiDiPointerClickCommandError { + /// The command identifier exceeds WebDriver BiDi's unsigned safe-integer range. + InvalidCommandId, + /// The browsing-context identifier is empty, over budget, or contains disallowed text. + InvalidBrowsingContext, +} + +impl Display for WebDriverBiDiPointerClickCommandError { + fn fmt(&self, formatter: &mut Formatter<'_>) -> std::fmt::Result { + formatter.write_str(match self { + Self::InvalidCommandId => "WebDriver BiDi command id is outside the js-uint range", + Self::InvalidBrowsingContext => { + "WebDriver BiDi browsing context is empty, over budget, or contains disallowed text" + } + }) + } +} + +impl Error for WebDriverBiDiPointerClickCommandError {} + +/// Deterministic command for one primary-button click on an admitted remote node. +/// +/// The fixed mouse action sequence moves to the element origin, presses button zero, and releases +/// button zero. Construction accepts an already admitted remote node reference and does not grant +/// browser-session, context, origin, document-epoch, policy, approval, or Agent authority. A trusted +/// adapter must bind this inert command to current authority before transport. +#[derive(Debug, PartialEq, Eq)] +pub struct WebDriverBiDiPointerClickCommand { + command_id: u64, + browsing_context: String, + json: String, +} + +impl WebDriverBiDiPointerClickCommand { + /// Validate and serialize one bounded `input.performActions` pointer click command. + pub fn new( + command_id: u64, + browsing_context: &str, + node: &crate::WebDriverBiDiRemoteNodeReference, + ) -> Result { + if command_id > MAX_WEBDRIVER_BIDI_COMMAND_ID { + return Err(WebDriverBiDiPointerClickCommandError::InvalidCommandId); + } + if browsing_context.is_empty() + || browsing_context.len() > MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES + || contains_disallowed_protocol_text(browsing_context, false) + { + return Err(WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext); + } + + let mut json = String::from("{\"id\":"); + json.push_str(&command_id.to_string()); + json.push_str(",\"method\":\""); + json.push_str(WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD); + json.push_str("\",\"params\":{\"context\":"); + push_json_string(&mut json, browsing_context); + json.push_str(",\"actions\":[{\"type\":\"pointer\",\"id\":\"originweave-mouse\",\"parameters\":{\"pointerType\":\"mouse\"},\"actions\":[{\"type\":\"pointerMove\",\"x\":0,\"y\":0,\"origin\":{\"type\":\"element\",\"element\":{\"sharedId\":"); + push_json_string(&mut json, node.shared_id()); + json.push_str("}}},{\"type\":\"pointerDown\",\"button\":0},{\"type\":\"pointerUp\",\"button\":0}]}]}}"); + + Ok(Self { + command_id, + browsing_context: browsing_context.to_owned(), + json, + }) + } + + /// Return the validated command identifier. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } + + /// Return the exact WebDriver BiDi method serialized by this command. + #[must_use] + pub const fn method(&self) -> &'static str { + WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD + } + + /// Return the exact validated browsing-context identifier. + #[must_use] + pub fn browsing_context(&self) -> &str { + &self.browsing_context + } + + /// Return the deterministic JSON command envelope. + #[must_use] + pub fn as_json(&self) -> &str { + &self.json + } +} + /// Fail-closed validation errors for one serialized WebDriver BiDi `locateNodes` command. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum WebDriverBiDiLocateNodesCommandError { diff --git a/crates/originweave-core/tests/webdriver_bidi_pointer_click_command.rs b/crates/originweave-core/tests/webdriver_bidi_pointer_click_command.rs new file mode 100644 index 000000000..dd32ce80c --- /dev/null +++ b/crates/originweave-core/tests/webdriver_bidi_pointer_click_command.rs @@ -0,0 +1,82 @@ +use std::error::Error; + +use originweave_core::{ + MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, MAX_WEBDRIVER_BIDI_COMMAND_ID, + UNICODE_PROTOCOL_FORMAT_INJECTION_CHARS, WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD, + WebDriverBiDiPointerClickCommand, WebDriverBiDiPointerClickCommandError, + WebDriverBiDiRemoteNodeReference, +}; + +#[test] +fn pointer_click_command_serializes_exact_bidi_envelope() -> Result<(), Box> { + let node = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; + let command = WebDriverBiDiPointerClickCommand::new(42, "context-a", &node)?; + + assert_eq!(command.command_id(), 42); + assert_eq!(command.method(), WEBDRIVER_BIDI_PERFORM_ACTIONS_METHOD); + assert_eq!(command.browsing_context(), "context-a"); + assert_eq!( + command.as_json(), + r#"{"id":42,"method":"input.performActions","params":{"context":"context-a","actions":[{"type":"pointer","id":"originweave-mouse","parameters":{"pointerType":"mouse"},"actions":[{"type":"pointerMove","x":0,"y":0,"origin":{"type":"element","element":{"sharedId":"shared-node-42"}}},{"type":"pointerDown","button":0},{"type":"pointerUp","button":0}]}]}}"# + ); + Ok(()) +} + +#[test] +fn pointer_click_command_rejects_invalid_command_and_context() -> Result<(), Box> { + let node = WebDriverBiDiRemoteNodeReference::new("node", Some("shared-node-42"))?; + + assert_eq!( + WebDriverBiDiPointerClickCommand::new( + MAX_WEBDRIVER_BIDI_COMMAND_ID + 1, + "context-a", + &node, + ), + Err(WebDriverBiDiPointerClickCommandError::InvalidCommandId) + ); + + for invalid in ["", "context with space", "context\nline"] { + assert_eq!( + WebDriverBiDiPointerClickCommand::new(1, invalid, &node), + Err(WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext) + ); + } + + let overlong = "c".repeat(MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES + 1); + assert_eq!( + WebDriverBiDiPointerClickCommand::new(1, &overlong, &node), + Err(WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext) + ); + for character in UNICODE_PROTOCOL_FORMAT_INJECTION_CHARS { + let context = format!("context{character}"); + assert_eq!( + WebDriverBiDiPointerClickCommand::new(1, &context, &node), + Err(WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext) + ); + } + Ok(()) +} + +#[test] +fn pointer_click_command_accepts_maximum_context_and_escaped_shared_id() +-> Result<(), Box> { + let context = "c".repeat(MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES); + let node = WebDriverBiDiRemoteNodeReference::new("node", Some(r#"node-"quoted"\path"#))?; + let command = + WebDriverBiDiPointerClickCommand::new(MAX_WEBDRIVER_BIDI_COMMAND_ID, &context, &node)?; + + assert!(command.as_json().contains(&context)); + assert!(command.as_json().contains(r#"node-\"quoted\"\\path"#)); + Ok(()) +} + +#[test] +fn pointer_click_command_error_contract_is_source_free() { + for error in [ + WebDriverBiDiPointerClickCommandError::InvalidCommandId, + WebDriverBiDiPointerClickCommandError::InvalidBrowsingContext, + ] { + assert!(error.source().is_none()); + assert!(!error.to_string().is_empty()); + } +} diff --git a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs index 2b66551b0..6d279fdba 100644 --- a/crates/originweave-network/src/webdriver_bidi_command_correlation.rs +++ b/crates/originweave-network/src/webdriver_bidi_command_correlation.rs @@ -24,6 +24,8 @@ pub enum WebDriverBiDiCommandKind { SessionStatus, /// WebDriver BiDi `session.end`. SessionEnd, + /// WebDriver BiDi `input.performActions` pointer click. + PointerClick, } #[derive(Clone, Copy, Debug, Eq, PartialEq)] diff --git a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md index b94d1faff..3840efed4 100644 --- a/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md +++ b/docs/doctoring/webdriver-bidi-received-response-connection-provenance.md @@ -42,6 +42,12 @@ Fresh integration verification executes 13 focused received-message, response an ## Evidence and remaining risk +### Pointer-click child integration + +PR #256 predecessor `9f2e6f29be46371762e3031a97c1cac04720694f` lacked the current connection-provenance implementation and collected zero command-correlation release tests under native discovery. The expected-one assertion failed before ordinary integration of parent `3e7057443d7c9532ff526acb5eefe8cd4778c767`; the inherited contract then collected and passed. Its core command implementation, exports and four pointer-click tests are byte-identical to the predecessor. The sole child delta in the parent's correlation module remains the documented `PointerClick` command kind. Serialization does not prove a browser click, authorize input, or establish an observed page post-condition. + +Fresh child verification passed all four pointer-click command tests, 13 received-message/response/teardown tests, 142 Python contracts, compileall, and the complete Rust 1.97.1 format/check/workspace-test/strict-Clippy/rustdoc gates. Pinned coverage measured 1088 functions, 11077 lines, 14146 regions and 1210 branches at 100%, with the unstable branch-option warning retained. These are local integrated-tree results; hosted checks, independent approval where required and protected-main delivery remain unproven. + The repair includes a realistic two-connection regression for a foreign `session.end` success response and focused reader coverage for fragmented text, an interleaved control frame, malformed server framing, binary-message rejection, event/null-id correlation, and connection-generation exhaustion after verified peer admission. The original inline response-substitution and closure-substitution findings are resolved by the current source boundary, but exact-head CI and security workflows remain authoritative before any integration claim. The current scope deliberately does not infer browser-process ownership from the WebSocket connection and does not make protocol success an operational teardown post-condition. Process-exit and profile-removal evidence remain unavailable until their canonical runtime owners provide non-forgeable contracts.