Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
7e85a7e
test: reject session.status reply from replacement connection
seonghobae Sep 6, 2026
6b102c1
test: apply canonical status-provenance formatting
seonghobae Sep 6, 2026
e77150f
fix: bind session.status to sender connection
seonghobae Sep 6, 2026
5aa7d1a
merge: adopt session.status sender provenance
seonghobae Sep 6, 2026
9307eca
fix: bind session.status responses to connection
seonghobae Sep 6, 2026
0b68198
test: carry status response connection evidence
seonghobae Sep 6, 2026
cafa950
test: retain status connection provenance in hostile cases
seonghobae Sep 6, 2026
05cbc5c
test: require exact status response connection provenance
seonghobae Sep 6, 2026
f979972
merge: adopt session.status sender provenance
seonghobae Sep 6, 2026
0e1e47c
test: exercise received status boundary
seonghobae Sep 6, 2026
aadba11
docs: record session.status connection provenance
seonghobae Sep 6, 2026
583ffee
repair(network): restore BiDi connection provenance prerequisite
seonghobae Sep 6, 2026
3a8c01f
merge: adopt current session.status connection provenance prerequisite
seonghobae Sep 6, 2026
615d840
fix: format session.status connection provenance boundary
seonghobae Sep 6, 2026
65ac3ab
fix(network): keep response provenance in child slice
seonghobae Sep 6, 2026
f0f2e20
merge: adopt session.status parent ownership correction
seonghobae Sep 6, 2026
74536b2
test(network): cover sealed reader and correlation rejection paths
seonghobae Sep 7, 2026
9bdd116
test(network): use consumed transport evidence in rejection fixture
seonghobae Sep 7, 2026
804a7a5
test(network): exercise missing provenance through public status replies
seonghobae Sep 7, 2026
d5c7aea
test(network): preserve pending status across unroutable replies
seonghobae Sep 7, 2026
bbdc6ac
refactor(network): share non-consuming response routing validation
seonghobae Sep 7, 2026
edec535
test(network): reject replacement status replies in session end stack
seonghobae Sep 7, 2026
cbd07e1
merge: preserve session end sender on current status response parent
seonghobae Sep 7, 2026
924ad97
docs: bound session end parent adoption evidence
seonghobae Sep 7, 2026
6aaf7f3
test(network): reject replacement session-end replies
seonghobae Sep 7, 2026
e9d6794
merge: adopt current session-end command parent
seonghobae Sep 7, 2026
363a78e
fix(network): bind session-end replies to their sending connection
seonghobae Sep 7, 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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@ All notable changes to OriginWeave are documented in this file. The format follo

### Added

- Session-ending replies from a replacement connection can no longer complete the original pending request. The original reply remains usable, and a protocol acknowledgment still does not prove browser shutdown or cleanup.
- The session-ending command stack now retains the status-reply protections from its current parent. A reply from a replacement connection is rejected while the original pending status request remains recoverable; sending the end command still does not prove that the browser session ended.
- 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.
- Regression checks now exercise fragmented browser replies, interleaved control messages, and rejected replies without losing a pending request. These checks do not establish browser readiness or release acceptance.
- The typed browser-status response stack now includes its verified command and opening-exchange prerequisites, including the release-record check that previously did not execute; parsing remains bounded and does not grant browser authority or prove operational readiness.
- Bounded RFC 6455 WebDriver BiDi opening-response validation on the exact peer-verified stream: it admits only HTTP/1.1 `101`, case-insensitive `Upgrade`/`Connection` tokens, and the client-key-correlated `Sec-WebSocket-Accept` value within monotonic time and header-size ceilings; it restores blocking mode and still does not implement WebSocket frames or grant browser/Agent authority.
- Typed outbound WebDriver BiDi `session.status` over the bounded client WebSocket stream: it serializes only the standards-defined method with empty params, preserves exact typed command-id correlation, rejects invalid frame deadlines before registration, retires only the just-registered id when a local masking-key preflight proves no command bytes were emitted, and keeps correlation outstanding after partial or ambiguous writes; frame-write success is not treated as command completion or browser/Agent authority.
Expand Down
5 changes: 5 additions & 0 deletions crates/originweave-network/src/lib.rs

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔍 Legacy assembly path remains public

Raw frame reads and WebDriverBiDiWebSocketMessageAssembler still permit transport-detached assembly. Review whether retaining this public path leaves the provenance migration incomplete.

(Refers to this code)

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ mod connection;
mod webdriver_bidi_command_correlation;
mod webdriver_bidi_connection;
mod webdriver_bidi_json_envelope;
mod webdriver_bidi_received_message;
mod webdriver_bidi_session_end_command;
mod webdriver_bidi_session_end_response;
mod webdriver_bidi_session_status_command;
Expand Down Expand Up @@ -49,6 +50,10 @@ pub use webdriver_bidi_json_envelope::{
MAX_WEBDRIVER_BIDI_JS_UINT, MAX_WEBDRIVER_BIDI_JSON_DEPTH, WebDriverBiDiJsonEnvelope,
WebDriverBiDiJsonEnvelopeError, WebDriverBiDiJsonEnvelopeKind,
};
pub use webdriver_bidi_received_message::{
WebDriverBiDiConnectionMessageRead, WebDriverBiDiConnectionMessageReadError,
WebDriverBiDiReceivedTextMessage, WebDriverBiDiWebSocketMessageReader,
};
pub use webdriver_bidi_session_end_command::{
WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndCommandError,
};
Expand Down
175 changes: 140 additions & 35 deletions crates/originweave-network/src/webdriver_bidi_command_correlation.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ use std::{collections::BTreeMap, error::Error, fmt};

use crate::{
MAX_WEBDRIVER_BIDI_JS_UINT, WebDriverBiDiJsonEnvelope, WebDriverBiDiJsonEnvelopeRouting,
webdriver_bidi_connection::WebDriverBiDiConnectionGeneration,
};

/// Maximum number of local WebDriver BiDi commands retained as outstanding at once.
Expand All @@ -25,6 +26,12 @@ pub enum WebDriverBiDiCommandKind {
SessionEnd,
}

#[derive(Clone, Copy, Debug, Eq, PartialEq)]
struct OutstandingCommand {
kind: WebDriverBiDiCommandKind,
connection_generation: Option<WebDriverBiDiConnectionGeneration>,
}

/// Outcome of a response after it has consumed the matching outstanding command identifier.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebDriverBiDiCorrelatedResponseOutcome {
Expand All @@ -36,12 +43,15 @@ pub enum WebDriverBiDiCorrelatedResponseOutcome {

/// Credential-free evidence that one parsed response consumed one outstanding local command.
///
/// This value carries only the matched command identifier and success/error classification. It
/// does not retain result bodies, error text, browser authority, transport authority, or secrets.
/// This value carries only the matched command identifier and success/error classification. A
/// private process-local connection generation is retained when the command owner bound one before
/// I/O so later transport evidence can be compared without accepting caller-supplied provenance.
/// It does not retain result bodies, error text, browser authority, transport authority, or secrets.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WebDriverBiDiCorrelatedResponse {
command_id: u64,
outcome: WebDriverBiDiCorrelatedResponseOutcome,
connection_generation: Option<WebDriverBiDiConnectionGeneration>,
}

impl WebDriverBiDiCorrelatedResponse {
Expand Down Expand Up @@ -76,6 +86,16 @@ pub enum WebDriverBiDiCommandCorrelationError {
/// Command family actually registered for the outstanding identifier.
actual: WebDriverBiDiCommandKind,
},
/// A connection-bound consumer found an outstanding command with no connection provenance.
CommandConnectionProvenanceMissing {
/// Exact outstanding local command identifier.
command_id: u64,
},
/// The response was received on a different verified connection from the outstanding command.
ResponseConnectionMismatch {
/// Exact outstanding local command identifier left untouched after rejection.
command_id: u64,
},
/// An event is not a command response and cannot consume correlation state.
EventIsNotResponse,
/// A protocol error with a `null` id cannot be attributed to one outstanding command.
Expand All @@ -92,6 +112,12 @@ impl fmt::Display for WebDriverBiDiCommandCorrelationError {
Self::CommandKindMismatch { .. } => {
"WebDriver BiDi response command kind does not match the outstanding command"
}
Self::CommandConnectionProvenanceMissing { .. } => {
"WebDriver BiDi outstanding command lacks connection provenance"
}
Self::ResponseConnectionMismatch { .. } => {
"WebDriver BiDi response arrived on a different connection"
}
Self::EventIsNotResponse => "WebDriver BiDi event cannot be correlated as a response",
Self::UncorrelatableErrorResponse => {
"WebDriver BiDi error response has no correlatable command id"
Expand All @@ -106,14 +132,17 @@ impl Error for WebDriverBiDiCommandCorrelationError {}
/// Bounded local WebDriver BiDi command-response correlation state.
///
/// Register an id together with its exact typed command family only after the caller has committed
/// to that outbound command. A success or correlatable error response consumes the id exactly once
/// only through a matching typed consumer. Events, null-id errors, and command-kind mismatches leave
/// outstanding state untouched. This type performs no I/O, retry, command serialization, browser
/// authentication, or authority grant. Debug output reports only the outstanding-count summary;
/// command identifiers and command families remain private correlation state.
/// to that outbound command. Connection-owning command adapters may additionally bind the private
/// generation of the exact established transport before I/O. A success or correlatable error
/// response consumes the id exactly once only through a matching typed consumer. Events, null-id
/// errors, command-kind mismatches, missing connection provenance, and responses received on a
/// different verified connection leave outstanding state untouched. This type performs no I/O,
/// retry, command serialization, browser authentication, or authority grant. Debug output reports
/// only the outstanding-count summary; command identifiers, families, and generations remain
/// private correlation state.
#[derive(Default)]
pub struct WebDriverBiDiCommandCorrelation {
outstanding: BTreeMap<u64, WebDriverBiDiCommandKind>,
outstanding: BTreeMap<u64, OutstandingCommand>,
}

impl fmt::Debug for WebDriverBiDiCommandCorrelation {
Expand Down Expand Up @@ -147,6 +176,24 @@ impl WebDriverBiDiCommandCorrelation {
&mut self,
command_id: u64,
command_kind: WebDriverBiDiCommandKind,
) -> Result<(), WebDriverBiDiCommandCorrelationError> {
self.register(command_id, command_kind, None)
}

pub(crate) fn register_command_for_connection(
&mut self,
command_id: u64,
command_kind: WebDriverBiDiCommandKind,
connection_generation: WebDriverBiDiConnectionGeneration,
) -> Result<(), WebDriverBiDiCommandCorrelationError> {
self.register(command_id, command_kind, Some(connection_generation))
}

fn register(
&mut self,
command_id: u64,
command_kind: WebDriverBiDiCommandKind,
connection_generation: Option<WebDriverBiDiConnectionGeneration>,
) -> Result<(), WebDriverBiDiCommandCorrelationError> {
if command_id > MAX_WEBDRIVER_BIDI_JS_UINT {
return Err(WebDriverBiDiCommandCorrelationError::CommandIdOutOfRange);
Expand All @@ -157,7 +204,13 @@ impl WebDriverBiDiCommandCorrelation {
if self.outstanding.len() >= MAX_WEBDRIVER_BIDI_OUTSTANDING_COMMANDS {
return Err(WebDriverBiDiCommandCorrelationError::OutstandingCommandLimit);
}
let _previous = self.outstanding.insert(command_id, command_kind);
let _previous = self.outstanding.insert(
command_id,
OutstandingCommand {
kind: command_kind,
connection_generation,
},
);
Ok(())
}

Expand All @@ -179,51 +232,49 @@ impl WebDriverBiDiCommandCorrelation {
///
/// Successful responses and error responses with ids consume exactly one matching command.
/// Unknown ids and command-kind mismatches fail without consuming state. Events and null-id
/// errors fail before touching the map.
/// errors fail before touching the map. This generic path does not claim received-connection
/// provenance; connection-sensitive command owners must use their connection-bound path.
pub fn correlate_response_for(
&mut self,
envelope: &WebDriverBiDiJsonEnvelope,
expected_kind: WebDriverBiDiCommandKind,
) -> Result<WebDriverBiDiCorrelatedResponse, WebDriverBiDiCommandCorrelationError> {
match envelope.routing() {
WebDriverBiDiJsonEnvelopeRouting::Event => {
Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse)
}
WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => {
Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse)
}
WebDriverBiDiJsonEnvelopeRouting::CommandError {
command_id: Some(command_id),
} => self.complete(
command_id,
expected_kind,
WebDriverBiDiCorrelatedResponseOutcome::Error,
),
WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => self.complete(
command_id,
expected_kind,
WebDriverBiDiCorrelatedResponseOutcome::Success,
),
}
let (command_id, outcome) = response_route(envelope)?;
self.complete(command_id, expected_kind, outcome)
Comment on lines +242 to +243

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟥 Connection-bound replies remain bypassable

A caller can pass a replacement connection's matching envelope to correlate_response_for. The generic path consumes connection-bound commands without checking their stored generation. The replacement reply can therefore complete the protected request.

Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}

pub(crate) fn correlate_response_for_connection(
&mut self,
envelope: &WebDriverBiDiJsonEnvelope,
expected_kind: WebDriverBiDiCommandKind,
received_connection_generation: WebDriverBiDiConnectionGeneration,
) -> Result<WebDriverBiDiCorrelatedResponse, WebDriverBiDiCommandCorrelationError> {
let (command_id, outcome) = response_route(envelope)?;
self.complete_on_connection(
command_id,
expected_kind,
outcome,
received_connection_generation,
)
}

fn require_command_kind(
&self,
command_id: u64,
expected_kind: WebDriverBiDiCommandKind,
) -> Result<(), WebDriverBiDiCommandCorrelationError> {
) -> Result<OutstandingCommand, WebDriverBiDiCommandCorrelationError> {
let actual = self
.outstanding
.get(&command_id)
.copied()
.ok_or(WebDriverBiDiCommandCorrelationError::CommandNotOutstanding)?;
if actual != expected_kind {
if actual.kind != expected_kind {
return Err(WebDriverBiDiCommandCorrelationError::CommandKindMismatch {
expected: expected_kind,
actual,
actual: actual.kind,
});
}
Ok(())
Ok(actual)
}

fn complete(
Expand All @@ -232,15 +283,59 @@ impl WebDriverBiDiCommandCorrelation {
expected_kind: WebDriverBiDiCommandKind,
outcome: WebDriverBiDiCorrelatedResponseOutcome,
) -> Result<WebDriverBiDiCorrelatedResponse, WebDriverBiDiCommandCorrelationError> {
self.require_command_kind(command_id, expected_kind)?;
let outstanding = self.require_command_kind(command_id, expected_kind)?;
let _removed = self.outstanding.remove(&command_id);
Ok(WebDriverBiDiCorrelatedResponse {
command_id,
outcome,
connection_generation: outstanding.connection_generation,
})
}

fn complete_on_connection(
&mut self,
command_id: u64,
expected_kind: WebDriverBiDiCommandKind,
outcome: WebDriverBiDiCorrelatedResponseOutcome,
received_connection_generation: WebDriverBiDiConnectionGeneration,
) -> Result<WebDriverBiDiCorrelatedResponse, WebDriverBiDiCommandCorrelationError> {
let outstanding = self.require_command_kind(command_id, expected_kind)?;
let expected_connection_generation = outstanding.connection_generation.ok_or(
WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { command_id },
)?;
if expected_connection_generation != received_connection_generation {
return Err(
WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id },
);
}
let _removed = self.outstanding.remove(&command_id);
Ok(WebDriverBiDiCorrelatedResponse {
command_id,
outcome,
connection_generation: Some(expected_connection_generation),
})
}
}

fn response_route(
envelope: &WebDriverBiDiJsonEnvelope,
) -> Result<(u64, WebDriverBiDiCorrelatedResponseOutcome), WebDriverBiDiCommandCorrelationError> {
match envelope.routing() {
WebDriverBiDiJsonEnvelopeRouting::Event => {
Err(WebDriverBiDiCommandCorrelationError::EventIsNotResponse)
}
WebDriverBiDiJsonEnvelopeRouting::CommandError { command_id: None } => {
Err(WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse)
}
WebDriverBiDiJsonEnvelopeRouting::CommandError {
command_id: Some(command_id),
} => Ok((command_id, WebDriverBiDiCorrelatedResponseOutcome::Error)),
WebDriverBiDiJsonEnvelopeRouting::CommandSuccess { command_id } => {
Ok((command_id, WebDriverBiDiCorrelatedResponseOutcome::Success))
}
}
}

#[cfg(test)]
mod tests {
use super::{WebDriverBiDiCommandCorrelationError, WebDriverBiDiCommandKind};
Expand Down Expand Up @@ -271,6 +366,16 @@ mod tests {
},
"WebDriver BiDi response command kind does not match the outstanding command",
),
(
WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing {
command_id: 7,
},
"WebDriver BiDi outstanding command lacks connection provenance",
),
(
WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { command_id: 7 },
"WebDriver BiDi response arrived on a different connection",
),
(
WebDriverBiDiCommandCorrelationError::EventIsNotResponse,
"WebDriver BiDi event cannot be correlated as a response",
Expand Down
Loading
Loading