Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ All notable changes to OriginWeave are documented in this file. The format follo

## [Unreleased]

- Reject field-observation requests on another browser session and preserve pending requests only when a write may have reached the peer.

- Preserve fixed field-observation checks while adopting current input safeguards; constructing a request still does not verify that the field changed.
- Label earlier text-reply limitations as historical so they do not contradict the later click safeguards.
- Preserve text-reply checks while adopting click-session and reply safeguards. A matched response still does not prove the requested field changed.
Expand Down Expand Up @@ -55,6 +57,7 @@ All notable changes to OriginWeave are documented in this file. The format follo

### Added

- Typed WebDriver BiDi command-family correlation for text-value post-condition observations, so a matching numeric response id cannot consume an outstanding command from another operation family; successful envelopes reuse the parser-proven non-null id invariant without an unreachable fallback branch.
- 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 primary-button click transport over the bounded client WebSocket stream: it rejects invalid frame deadlines before correlation registration, retires only the just-registered id when local frame preflight proves no command bytes were emitted, preserves correlation across ambiguous writes, and does not treat frame-write success as proof that the browser performed the click.
- Typed pointer-click response admission that consumes only the exact outstanding command-kind correlation after complete envelope validation, keeps remote protocol errors distinct from success, and does not treat a protocol acknowledgment as proof that the target activated or the document changed.
Expand Down
6 changes: 5 additions & 1 deletion crates/originweave-network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
//! binds received fragmented text to one exact verified connection, classifies
//! complete local-end JSON envelopes, tracks bounded command-response correlation,
//! transports narrowly typed pointer-click and node-bound non-secret text-input
//! actions, admits typed correlated protocol responses, sends a context-bound committed-navigation subscription and retains
//! actions and fixed sandboxed text-value observations, admits typed correlated protocol responses, sends a context-bound committed-navigation subscription and retains
//! its typed bounded correlated identifier, binds navigation-event admission to
//! that active command/receipt lifecycle with bounded fail-closed navigation replay
//! prevention, explicitly unsubscribes that exact
Expand Down Expand Up @@ -49,6 +49,7 @@ mod webdriver_bidi_session_end_response;
mod webdriver_bidi_session_status_command;
mod webdriver_bidi_session_status_response;
mod webdriver_bidi_session_teardown;
mod webdriver_bidi_text_value_observation_transport;
mod webdriver_bidi_type_text_response;
mod webdriver_bidi_type_text_transport;
mod webdriver_bidi_websocket_frame;
Expand Down Expand Up @@ -146,6 +147,9 @@ pub use webdriver_bidi_session_teardown::{
WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownAssessmentError,
WebDriverBiDiSessionTeardownDisposition, WebDriverBiDiSessionTeardownObservations,
};
pub use webdriver_bidi_text_value_observation_transport::{
WebDriverBiDiTextValueObservationSendError, send_webdriver_bidi_text_value_observation,
};
pub use webdriver_bidi_type_text_response::{
WebDriverBiDiTypeTextResponseError, WebDriverBiDiTypeTextResult,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ pub const MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS: usize = 256;
/// the same id. Additional command families are introduced by their owning typed command slices.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebDriverBiDiCommandKind {
/// Fixed product-owned `script.callFunction` text-value observation.
TextValueObservation,
/// WebDriver BiDi `session.status`.
SessionStatus,
/// WebDriver BiDi `session.end`.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use std::{error::Error, fmt, time::Duration};

use originweave_core::{
AdmittedNodeHandle, BrowserAuthorityRegistry, BrowserProtocolCapability, BrowserProtocolKind,
ValidatedBrowserProtocolUse, WebDriverBiDiRemoteNodeReference,
WebDriverBiDiTextValueObservationAuthorityError, WebDriverBiDiTextValueObservationCommand,
};

use crate::{
WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError,
WebDriverBiDiCommandKind, WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrameError,
WebDriverBiDiWebSocketMaskKey,
};

/// Fail-closed errors while transporting one current-authority text-value observation command.
#[derive(Debug)]
pub enum WebDriverBiDiTextValueObservationSendError {
/// The supplied protocol-use proof belongs to another browser protocol family.
UnsupportedProtocolKind(BrowserProtocolKind),
/// The supplied protocol-use proof did not validate semantic-observation capability.
UnsupportedCapability(BrowserProtocolCapability),
/// Current node, browser-context, document, or bounded command authority failed revalidation.
Authority {
/// Exact typed immediate-use authority failure.
source: WebDriverBiDiTextValueObservationAuthorityError,
},
/// The bounded correlation registry rejected the command before network I/O.
Correlation {
/// Exact typed correlation failure.
source: WebDriverBiDiCommandCorrelationError,
},
/// Deadline validation or writing the command frame failed.
FrameWrite {
/// Exact typed bounded WebSocket frame-write failure.
source: WebDriverBiDiWebSocketFrameError,
},
}

impl fmt::Display for WebDriverBiDiTextValueObservationSendError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str(match self {
Self::UnsupportedProtocolKind(_) => {
"WebDriver BiDi text-value observation send requires a WebDriver BiDi proof"
}
Self::UnsupportedCapability(_) => {
"WebDriver BiDi text-value observation send requires semantic-observation capability"
}
Self::Authority { .. } => {
"WebDriver BiDi text-value observation authority was rejected"
}
Self::Correlation { .. } => {
"WebDriver BiDi text-value observation command correlation was rejected"
}
Self::FrameWrite { .. } => {
"WebDriver BiDi text-value observation command frame write failed"
}
})
}
}

impl Error for WebDriverBiDiTextValueObservationSendError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
Self::UnsupportedProtocolKind(_) | Self::UnsupportedCapability(_) => None,
Self::Authority { source } => Some(source),
Self::Correlation { source } => Some(source),
Self::FrameWrite { source } => Some(source),
}
}
}

/// Revalidate, register, and write one fixed text-value `script.callFunction` observation.
///
/// The caller must transfer a non-cloneable [`ValidatedBrowserProtocolUse`] whose protocol family
/// is exactly [`BrowserProtocolKind::WebDriverBiDi`] and whose capability is exactly
/// [`BrowserProtocolCapability::SemanticObservation`]. The proof is consumed before node
/// authority, command correlation, or frame I/O, so typed-input, navigation, CDP, or other
/// protocol proofs cannot dispatch this observation through this boundary.
///
/// Immediately before correlation, this boundary reconstructs the fixed product-owned command
/// from the [`AdmittedNodeHandle`], exact external browsing-context identifier, remote node
/// reference, and live [`BrowserAuthorityRegistry`]. The core constructor revalidates current
/// session, context, origin, document epoch, registry provenance, and exact admitted wire node
/// identifier. Callers cannot supply function source, sandbox, or generic script arguments.
///
/// The registered session must match the established connection's exact session identifier.
/// Deadline validation precedes registration. Registration binds the connection generation and
/// records [`WebDriverBiDiCommandKind::TextValueObservation`] before the first
/// possible remote side effect. A later typed response boundary must match both the exact id and
/// this command provenance; an unrelated outstanding command id therefore cannot certify a text
/// post-condition. A correlation failure writes nothing. A proven zero-write malformed-frame
/// rejection retires only this registration; an ambiguous write failure keeps it outstanding.
/// The transport enforces increasing command identifiers across its entire lifetime.
///
/// Dispatch is only protocol-level observation transport. This function does not authenticate the
/// browser, authorize the preceding text-input action, compare the eventual remote value with the
/// intended non-secret text, infer post-condition success, retry, reconnect, select another
/// destination, or grant policy, destination, secret, process, profile, or Agent authority.
#[expect(
clippy::too_many_arguments,
reason = "this immediate-use security boundary keeps command identity, live node authority, transport, correlation, masking, and deadline inputs explicit rather than persisting a reusable prevalidated command"
)]
pub fn send_webdriver_bidi_text_value_observation(
validated: ValidatedBrowserProtocolUse,
command_id: u64,
browsing_context: &str,
handle: &AdmittedNodeHandle,
node: &WebDriverBiDiRemoteNodeReference,
registry: &BrowserAuthorityRegistry,
established: WebDriverBiDiWebSocketEstablished,
correlation: &mut WebDriverBiDiCommandCorrelation,
masking_key: WebDriverBiDiWebSocketMaskKey,
frame_timeout: Duration,
) -> Result<WebDriverBiDiWebSocketEstablished, WebDriverBiDiTextValueObservationSendError> {
if validated.kind() != BrowserProtocolKind::WebDriverBiDi {
return Err(
WebDriverBiDiTextValueObservationSendError::UnsupportedProtocolKind(validated.kind()),
);
}
if validated.capability() != BrowserProtocolCapability::SemanticObservation {
return Err(
WebDriverBiDiTextValueObservationSendError::UnsupportedCapability(
validated.capability(),
),
);
}
let _consumed_semantic_observation_proof = validated;

let command = WebDriverBiDiTextValueObservationCommand::new_for_current_node(
command_id,
browsing_context,
handle,
node,
registry,
)
.map_err(|source| WebDriverBiDiTextValueObservationSendError::Authority { source })?;

registry
.require_registered_session_external_identifier(
handle.browser_session(),
established
.transport_evidence()
.verified_peer()
.session_id(),
)
.map_err(
|source| WebDriverBiDiTextValueObservationSendError::Authority {
source: WebDriverBiDiTextValueObservationAuthorityError::BrowserAuthority(source),
},
)?;
crate::webdriver_bidi_websocket_frame::validate_frame_timeout(frame_timeout)
.map_err(|source| WebDriverBiDiTextValueObservationSendError::FrameWrite { source })?;
correlation
.register_command_for_connection(
command.command_id(),
WebDriverBiDiCommandKind::TextValueObservation,
established.transport_evidence().connection_generation(),
)
.map_err(|source| WebDriverBiDiTextValueObservationSendError::Correlation { source })?;
established
.write_command_frame(
command.command_id(),
command.as_json(),
masking_key,
frame_timeout,
)
.map_err(|source| {
if matches!(
source,
WebDriverBiDiWebSocketFrameError::MalformedFrame { .. }
) {
let _retirement = correlation.retire_command_for(
command.command_id(),
WebDriverBiDiCommandKind::TextValueObservation,
);
}
WebDriverBiDiTextValueObservationSendError::FrameWrite { source }
})
}
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,39 @@ fn parse_success_over_loopback() -> Result<WebDriverBiDiJsonEnvelope, Box<dyn Er
Ok(envelope)
}

#[test]
fn observation_response_cannot_cross_any_sibling_command_family() -> Result<(), Box<dyn Error>> {
let response = parse_success_over_loopback()?;
for sibling in [
WebDriverBiDiCommandKind::SessionStatus,
WebDriverBiDiCommandKind::SessionEnd,
WebDriverBiDiCommandKind::PointerClick,
WebDriverBiDiCommandKind::TypeText,
WebDriverBiDiCommandKind::NavigationCommittedSubscription,
WebDriverBiDiCommandKind::NavigationCommittedUnsubscribe,
] {
for (actual, expected) in [
(WebDriverBiDiCommandKind::TextValueObservation, sibling),
(sibling, WebDriverBiDiCommandKind::TextValueObservation),
] {
let mut correlation = WebDriverBiDiCommandCorrelation::new();
correlation.register_command_for(42, actual)?;
assert_eq!(
correlation.correlate_response_for(&response, expected),
Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch { expected, actual })
);
assert_eq!(correlation.outstanding_count(), 1);
assert_eq!(
correlation
.correlate_response_for(&response, actual)?
.command_id(),
42
);
}
}
Ok(())
}

#[test]
fn response_cannot_consume_a_different_outstanding_command_kind() -> Result<(), Box<dyn Error>> {
let mut correlation = WebDriverBiDiCommandCorrelation::new();
Expand Down
Loading
Loading