Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
48ad304
test(core): require explicit BiDi connect target
seonghobae Aug 19, 2026
3cc6711
test(core): format explicit BiDi connect target regressions
seonghobae Aug 19, 2026
2e9898d
feat(core): derive explicit BiDi loopback connect targets
seonghobae Aug 19, 2026
aace3a8
feat(core): export explicit BiDi connect target contract
seonghobae Aug 19, 2026
2e43ad6
style(core): format explicit BiDi connect target
seonghobae Aug 19, 2026
6a21bed
test(core): require BiDi connect target changelog evidence
seonghobae Aug 19, 2026
7e30360
docs(core): record explicit BiDi connect target boundary
seonghobae Aug 19, 2026
dc5cfd8
test(core): preserve BiDi endpoint across resolver handoff
seonghobae Aug 19, 2026
3a9506c
fix(core): retain correlated BiDi endpoint on resolver handoff
seonghobae Aug 19, 2026
d34c528
test(core): require exact BiDi socket peer verification
seonghobae Aug 19, 2026
d7cf226
feat(core): verify exact BiDi socket peer
seonghobae Aug 19, 2026
89fc9c3
feat(core): export verified BiDi peer contract
seonghobae Aug 19, 2026
f62d204
test(core): require BiDi socket-peer release evidence
seonghobae Aug 19, 2026
d52cdfe
docs(changelog): record BiDi socket-peer verification
seonghobae Aug 19, 2026
fa510a0
fix(changelog): preserve direct TCP release wording
seonghobae Aug 19, 2026
d8f45f8
merge: carry ChromeDriver session compatibility into connect targets
seonghobae Aug 21, 2026
036412a
merge: carry ChromeDriver session compatibility into socket peer veri…
seonghobae Aug 21, 2026
1db9faa
Merge pull request #191 from ContextualWisdomLab/feat/webdriver-bidi-…
seonghobae Aug 26, 2026
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ 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.
- Canonical OriginWeave protocol-version parsing for exact `originweave/<major>.<minor>` syntax, with typed fail-closed rejection of malformed, ambiguous, overflowed, or noncanonical serialized generations; parsing does not negotiate compatibility or grant adapter authority.
Expand Down
5 changes: 5 additions & 0 deletions crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -88,6 +89,10 @@ pub use webdriver_bidi_response_envelope::{
pub use webdriver_bidi_result::{
ValidatedWebDriverBiDiLocateNodesResult, WebDriverBiDiLocateNodesResultAdmissionError,
};
pub use webdriver_bidi_websocket_connect_target::{
VerifiedWebDriverBiDiSocketPeer, WebDriverBiDiSocketPeerVerificationError,
WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketConnectTargetError,
};
pub use webdriver_bidi_websocket_endpoint::{
CorrelatedWebDriverBiDiWebSocketEndpoint, MAX_WEBDRIVER_BIDI_WEBSOCKET_ENDPOINT_BYTES,
WebDriverBiDiWebSocketEndpoint, WebDriverBiDiWebSocketEndpointAdmissionError,
Expand Down
206 changes: 206 additions & 0 deletions crates/originweave-core/src/webdriver_bidi_websocket_connect_target.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,206 @@
//! 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. When explicit trusted name resolution is needed,
//! the typed error preserves the correlated endpoint instead of discarding its session evidence.
//! 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,
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 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,
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
}

/// 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<VerifiedWebDriverBiDiSocketPeer, WebDriverBiDiSocketPeerVerificationError> {
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.
///
/// 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. 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<WebDriverBiDiWebSocketConnectTarget, WebDriverBiDiWebSocketConnectTargetError> {
let socket_addr = if let Ok(ipv4) = self.host().parse::<Ipv4Addr>() {
SocketAddr::from((ipv4, self.port()))
} else if let Ok(ipv6) = self.host().parse::<Ipv6Addr>() {
SocketAddr::from((ipv6, self.port()))
Comment thread
seonghobae marked this conversation as resolved.
} else {
return Err(
WebDriverBiDiWebSocketConnectTargetError::NameResolutionRequired {
correlated_endpoint: self,
},
);
};
Comment thread
seonghobae marked this conversation as resolved.

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, PartialEq, Eq)]
pub enum WebDriverBiDiWebSocketConnectTargetError {
/// The admitted endpoint used a host name and therefore requires explicit trusted resolution.
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(
"WebDriver BiDi WebSocket endpoint requires explicit trusted name resolution",
),
}
}
}

impl std::error::Error for WebDriverBiDiWebSocketConnectTargetError {}
Original file line number Diff line number Diff line change
@@ -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<CorrelatedWebDriverBiDiWebSocketEndpoint, _> =
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());
}
Loading
Loading