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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ All notable changes to OriginWeave are documented in this file. The format follo

- 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 keeps the correlated protocol acknowledgment separate from explicit transport-closure, browser-process-exit, and task-profile-removal observations, reporting completion only when all three are present and without treating caller-supplied observations as authenticated evidence.
- 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 Expand Up @@ -59,6 +60,7 @@ All notable changes to OriginWeave are documented in this file. The format follo

### Changed

- Carried current response prerequisites and the executable release-record check into the teardown-assessment stack; caller-supplied cleanup claims remain unverified and cannot establish operational acceptance.
- Carried verified command prerequisites and the executable release-record check into session-end response validation without changing response admission or treating an acknowledgment as proof of resource cleanup.
- Carried the verified status-response prerequisites into the session-end sender, preserving its command behavior and making the inherited release-record check execute in the existing test suite.
- Kept the `session.status` frame-failure coverage contract focused on observable correlation state, avoiding assertion-internal uncovered branches without weakening preflight retirement or ambiguous-write retention checks.
Expand Down
10 changes: 8 additions & 2 deletions crates/originweave-network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,9 @@
//! unmasked server-frame reads, assembles bounded WebDriver BiDi text messages,
//! classifies complete local-end JSON envelopes, tracks bounded command-response
//! correlation, sends narrowly typed `session.status` and `session.end` commands,
//! and admits typed correlated status and end responses without exposing generic
//! JSON bodies or granting browser, TLS, policy, secret, or Agent authority.
//! admits typed correlated status and end responses, and keeps protocol teardown
//! acknowledgment separate from explicit operational teardown observations without
//! exposing generic JSON bodies or granting browser, TLS, policy, secret, or Agent authority.

#![forbid(unsafe_code)]
#![deny(missing_docs)]
Expand All @@ -23,6 +24,7 @@ mod webdriver_bidi_session_end_command;
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_websocket_frame;
mod webdriver_bidi_websocket_handshake;
mod webdriver_bidi_websocket_message;
Expand Down Expand Up @@ -62,6 +64,10 @@ pub use webdriver_bidi_session_status_response::{
MAX_WEBDRIVER_BIDI_SESSION_STATUS_MESSAGE_SIZE, WebDriverBiDiSessionStatusResponseError,
WebDriverBiDiSessionStatusResult,
};
pub use webdriver_bidi_session_teardown::{
WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownDisposition,
WebDriverBiDiSessionTeardownObservations,
};
pub use webdriver_bidi_websocket_frame::{
MAX_WEBSOCKET_FRAME_PAYLOAD_SIZE, MAX_WEBSOCKET_FRAME_TIMEOUT,
WebDriverBiDiWebSocketEstablished, WebDriverBiDiWebSocketFrame,
Expand Down
119 changes: 119 additions & 0 deletions crates/originweave-network/src/webdriver_bidi_session_teardown.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
use crate::WebDriverBiDiSessionEndResult;

/// Fail-closed operational disposition derived from explicit teardown observations.
///
/// This value does not authenticate any observation or grant process, profile, browser, network,
/// policy, or Agent authority. `OperationallyComplete` means only that the caller supplied all
/// reviewed observation classes after a correlated `session.end` protocol acknowledgment.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub enum WebDriverBiDiSessionTeardownDisposition {
/// One or more required operational teardown observations remain absent.
OperationalTeardownPending,
/// Every required operational teardown observation was supplied.
OperationallyComplete,
}

/// Explicit operational observations required after a correlated WebDriver BiDi `session.end` ack.
///
/// These booleans are deliberately observation facts, not authority or evidence provenance. The
/// trusted browser/process/profile owner remains responsible for producing and authenticating the
/// underlying observations before constructing this value.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WebDriverBiDiSessionTeardownObservations {
transport_closed_observed: bool,
browser_process_exited_observed: bool,
task_profile_removed_observed: bool,
}

impl WebDriverBiDiSessionTeardownObservations {
/// Construct the three explicit operational observations required by this boundary.
#[must_use]
pub const fn new(
transport_closed_observed: bool,
browser_process_exited_observed: bool,
task_profile_removed_observed: bool,
) -> Self {
Self {
transport_closed_observed,
browser_process_exited_observed,
task_profile_removed_observed,
}
}

/// Return whether closure of the exact session transport was observed.
#[must_use]
pub const fn transport_closed_observed(&self) -> bool {
self.transport_closed_observed
}

/// Return whether exit of the owned browser process was observed.
#[must_use]
pub const fn browser_process_exited_observed(&self) -> bool {
self.browser_process_exited_observed
}

/// Return whether removal of the owned task profile was observed.
#[must_use]
pub const fn task_profile_removed_observed(&self) -> bool {
self.task_profile_removed_observed
}

const fn operationally_complete(&self) -> bool {
self.transport_closed_observed
& self.browser_process_exited_observed
& self.task_profile_removed_observed
}
}

/// One correlated `session.end` acknowledgment kept separate from operational teardown evidence.
///
/// A protocol acknowledgment alone is never operational completion. Callers must separately
/// provide all reviewed transport/process/profile observations, and the observations themselves
/// remain non-authoritative until authenticated by their owning runtime boundary.
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
pub struct WebDriverBiDiSessionTeardownAssessment {
protocol_ack: WebDriverBiDiSessionEndResult,
observations: WebDriverBiDiSessionTeardownObservations,
}

impl WebDriverBiDiSessionTeardownAssessment {
/// Bind one correlated protocol acknowledgment to separately supplied operational observations.
#[must_use]
pub const fn from_protocol_ack(
protocol_ack: WebDriverBiDiSessionEndResult,
observations: WebDriverBiDiSessionTeardownObservations,
) -> Self {
Self {
protocol_ack,
observations,
}
}

/// Return the exact command id proven by the correlated protocol acknowledgment.
#[must_use]
pub const fn command_id(&self) -> u64 {
self.protocol_ack.command_id()
}

/// Borrow the explicit operational observations bound to this assessment.
#[must_use]
pub const fn observations(&self) -> &WebDriverBiDiSessionTeardownObservations {
&self.observations
}

/// Return whether all required operational teardown observations are present.
#[must_use]
pub const fn is_operationally_complete(&self) -> bool {
self.observations.operationally_complete()
}

/// Return the fail-closed disposition for the currently supplied observations.
#[must_use]
pub const fn disposition(&self) -> WebDriverBiDiSessionTeardownDisposition {
if self.is_operationally_complete() {
WebDriverBiDiSessionTeardownDisposition::OperationallyComplete
} else {
WebDriverBiDiSessionTeardownDisposition::OperationalTeardownPending
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
use std::{
error::Error,
io::{self, Read, Write},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};

use originweave_core::WebDriverBiDiWebSocketEndpoint;
use originweave_network::{
WebDriverBiDiCommandCorrelation, WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResult,
WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownDisposition,
WebDriverBiDiSessionTeardownObservations, WebDriverBiDiTcpConnectionPlan,
WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan,
WebDriverBiDiWebSocketMaskKey, WebDriverBiDiWebSocketMessageAssembler,
WebDriverBiDiWebSocketMessageAssembly,
};

const SESSION_ID: &str = "01234567-89ab-cdef-0123-456789abcdef";
const RFC6455_SAMPLE_KEY: &str = "dGhlIHNhbXBsZSBub25jZQ==";
const OPENING_RESPONSE: &[u8] = b"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=\r\n\r\n";
const END_SUCCESS_RESPONSE: &[u8] = br#"{"type":"success","id":7,"result":{}}"#;

fn read_opening_request(stream: &mut TcpStream) -> io::Result<()> {
stream.set_read_timeout(Some(Duration::from_secs(2)))?;
let mut request = Vec::new();
let mut buffer = [0_u8; 512];
while !request.ends_with(b"\r\n\r\n") {
let count = stream.read(&mut buffer)?;
if count == 0 {
return Err(io::Error::new(
io::ErrorKind::UnexpectedEof,
"client opening request ended before the header terminator",
));
}
request.extend_from_slice(&buffer[..count]);
}
Ok(())
}

fn read_masked_text_frame(stream: &mut TcpStream) -> io::Result<Vec<u8>> {
let mut header = [0_u8; 2];
stream.read_exact(&mut header)?;
if header[0] != 0x81 || header[1] & 0x80 == 0 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"expected one final masked client text frame",
));
}
let length = usize::from(header[1] & 0x7f);
if length > 125 {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"session.end command unexpectedly required extended framing",
));
}
let mut mask = [0_u8; 4];
stream.read_exact(&mut mask)?;
let mut payload = vec![0_u8; length];
stream.read_exact(&mut payload)?;
for (index, byte) in payload.iter_mut().enumerate() {
*byte ^= mask[index % mask.len()];
}
Ok(payload)
}

fn correlated_session_end_ack() -> Result<WebDriverBiDiSessionEndResult, Box<dyn Error>> {
let listener = TcpListener::bind(("127.0.0.1", 0))?;
let local_addr = listener.local_addr()?;
let server = thread::spawn(move || -> io::Result<()> {
let (mut stream, _) = listener.accept()?;
read_opening_request(&mut stream)?;
stream.write_all(OPENING_RESPONSE)?;
let command = read_masked_text_frame(&mut stream)?;
if command != br#"{"id":7,"method":"session.end","params":{}}"# {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"unexpected session.end command",
));
}
stream.write_all(&[0x81, END_SUCCESS_RESPONSE.len() as u8])?;
stream.write_all(END_SUCCESS_RESPONSE)
});

let endpoint = format!("ws://{local_addr}/session/{SESSION_ID}");
let target = WebDriverBiDiWebSocketEndpoint::new(&endpoint)?
.correlate_session_id(SESSION_ID)?
.into_explicit_connect_target()?;
let connection =
WebDriverBiDiTcpConnectionPlan::new(target, Duration::from_secs(1), 1)?.connect()?;
let key = WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?;
let established = WebDriverBiDiWebSocketHandshakePlan::new(connection, key)?
.write_opening_request(Duration::from_millis(500))?
.read_opening_response(Duration::from_millis(500))?;

let mut correlation = WebDriverBiDiCommandCorrelation::new();
let established = WebDriverBiDiSessionEndCommand::new(7)?.send(
established,
&mut correlation,
WebDriverBiDiWebSocketMaskKey::new([1, 2, 3, 4]),
Duration::from_millis(500),
)?;
let (_established, frame) = established.read_frame(Duration::from_millis(500))?;
let mut assembler = WebDriverBiDiWebSocketMessageAssembler::new();
let text = match assembler.push_frame(frame)? {
WebDriverBiDiWebSocketMessageAssembly::Text(text) => text,
other => {
return Err(io::Error::other(format!(
"session.end response produced unexpected assembly state: {other:?}"
))
.into());
}
};
server
.join()
.map_err(|_| io::Error::other("session.end teardown test server panicked"))??;
Ok(WebDriverBiDiSessionEndResult::parse_and_correlate(
&text,
&mut correlation,
)?)
}

#[test]
fn every_missing_operational_observation_keeps_teardown_pending() -> Result<(), Box<dyn Error>> {
let incomplete_observations = [
(false, true, true),
(true, false, true),
(true, true, false),
];

for (transport_closed, browser_exited, profile_removed) in incomplete_observations {
let acknowledged = correlated_session_end_ack()?;
let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack(
acknowledged,
WebDriverBiDiSessionTeardownObservations::new(
transport_closed,
browser_exited,
profile_removed,
),
);

assert_eq!(assessment.command_id(), 7);
assert_eq!(
assessment.observations().transport_closed_observed(),
transport_closed
);
assert_eq!(
assessment.observations().browser_process_exited_observed(),
browser_exited
);
assert_eq!(
assessment.observations().task_profile_removed_observed(),
profile_removed
);
assert!(!assessment.is_operationally_complete());
assert_eq!(
assessment.disposition(),
WebDriverBiDiSessionTeardownDisposition::OperationalTeardownPending
);
}
Ok(())
}

#[test]
fn correlated_ack_plus_all_operational_observations_is_complete() -> Result<(), Box<dyn Error>> {
let acknowledged = correlated_session_end_ack()?;
let assessment = WebDriverBiDiSessionTeardownAssessment::from_protocol_ack(
acknowledged,
WebDriverBiDiSessionTeardownObservations::new(true, true, true),
);

assert!(assessment.is_operationally_complete());
assert_eq!(
assessment.disposition(),
WebDriverBiDiSessionTeardownDisposition::OperationallyComplete
);
assert_eq!(assessment.command_id(), 7);
Ok(())
}
6 changes: 6 additions & 0 deletions docs/doctoring.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,12 @@ The #251 integration adopts #250 `ec433b844a121f8554c062f92267991af9cacb6f` by o

PR #252 ordinarily adopts current command parent `f02af6d0dd01708d495cc08dec785675f3d58898` while preserving the response implementation, public exports and four real-loopback response tests from `2015259529ada99af836989079cc85a15779a2d8`. The pre-integration native loader again collected zero correlation release-record checks; adopting the existing parent TestCase makes that contract executable without a new framework or copied owner fix. Both sides of the changelog-only conflict are retained. The response boundary still validates the entire bounded envelope before consuming exact typed correlation, retains remote errors as failures and leaves malformed or mismatched responses unable to consume another command. The acknowledgment remains unbound to received-connection provenance in this layer and does not prove process exit, profile removal or operational teardown. Later connection-bound evidence belongs to its own owner stack; local quality results, hosted checks and protected delivery remain separate.

### Teardown-assessment parent integration and acceptance limit

PR #253 ordinarily adopts response parent `6569bf40b6595ac74c2f0a997d202137f07ba1db` and preserves its assessment implementation, public exports and existing loopback tests from `0d72082e595c0e1fcc03d609ba337896ed14e2fc`. Native release-record discovery first failed with zero collected checks; the canonical parent provides the existing executable TestCase and synchronized opening fixtures. Both changelog records remain. No new assessment API or runtime-evidence producer is introduced by this integration.

The retained assessment still accepts three caller-supplied booleans and can label them `OperationallyComplete`; that calculation authenticates none of the observations and is not trusted operational-completion evidence. This known product gap remains open despite passing local structural tests or numerical coverage. The later #255 owner removes raw process/profile completion claims and binds received-response and closure provenance. That owner repair must remain intact when this dependency chain is integrated and independently reverified. No release or protected-main acceptance of caller claims is justified by this intermediate parent adoption.

## References

Amazon Web Services. (n.d.). *Set up the Amazon EKS Pod Identity Agent*. Retrieved August 6, 2026, from https://docs.aws.amazon.com/eks/latest/userguide/pod-id-agent-setup.html
Expand Down
Loading