diff --git a/CHANGELOG.md b/CHANGELOG.md index ad12a72bc..6127f0374 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,8 +6,10 @@ 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. @@ -60,6 +62,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- 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. - Made the command-correlation release-record check run in the existing CI test suite, preserving its exact bounds and authority exclusions; carried the verified message-parent fixture repairs into the correlation stack. diff --git a/crates/originweave-network/src/lib.rs b/crates/originweave-network/src/lib.rs index ac8e7f79d..b2cc56cb1 100644 --- a/crates/originweave-network/src/lib.rs +++ b/crates/originweave-network/src/lib.rs @@ -9,9 +9,8 @@ //! 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 the required readiness result through one command-specific correlated -//! parser without exposing generic JSON bodies or granting browser, TLS, policy, -//! secret, or Agent authority. +//! and admits typed correlated status and end responses without exposing generic +//! JSON bodies or granting browser, TLS, policy, secret, or Agent authority. #![forbid(unsafe_code)] #![deny(missing_docs)] @@ -22,6 +21,7 @@ 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; mod webdriver_bidi_session_status_response; mod webdriver_bidi_websocket_frame; @@ -57,6 +57,9 @@ pub use webdriver_bidi_received_message::{ pub use webdriver_bidi_session_end_command::{ WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndCommandError, }; +pub use webdriver_bidi_session_end_response::{ + WebDriverBiDiSessionEndResponseError, WebDriverBiDiSessionEndResult, +}; pub use webdriver_bidi_session_status_command::{ WebDriverBiDiSessionStatusCommand, WebDriverBiDiSessionStatusCommandError, }; diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs index 29fbe05aa..e9d3f7174 100644 --- a/crates/originweave-network/src/webdriver_bidi_session_end_command.rs +++ b/crates/originweave-network/src/webdriver_bidi_session_end_command.rs @@ -41,7 +41,8 @@ impl WebDriverBiDiSessionEndCommand { /// Register and write this exact command on an already established verified BiDi stream. /// /// Locally invalid frame deadlines fail before correlation registration and before any remote - /// side effect. Correlation then registers the command before the first possible frame write. + /// side effect. Correlation then binds the command to this connection before the first possible + /// frame write. Only a reply received on this same connection can complete that registration. /// A frame-owner preflight rejection that proves no write began retires this exact command /// again. Once frame emission can have begun, a later failure leaves the identifier outstanding /// because partial or full emission is ambiguous. A successful write also leaves the identifier @@ -62,7 +63,11 @@ impl WebDriverBiDiSessionEndCommand { }); } correlation - .register_command_for(self.command_id, WebDriverBiDiCommandKind::SessionEnd) + .register_command_for_connection( + self.command_id, + WebDriverBiDiCommandKind::SessionEnd, + established.transport_evidence().connection_generation(), + ) .map_err(|source| WebDriverBiDiSessionEndCommandError::Correlation { source })?; let message = self.serialized(); match established.write_text_frame(&message, masking_key, frame_timeout) { diff --git a/crates/originweave-network/src/webdriver_bidi_session_end_response.rs b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs new file mode 100644 index 000000000..cd284f134 --- /dev/null +++ b/crates/originweave-network/src/webdriver_bidi_session_end_response.rs @@ -0,0 +1,142 @@ +use std::{error::Error, fmt}; + +use crate::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiCommandKind, WebDriverBiDiCorrelatedResponseOutcome, WebDriverBiDiJsonEnvelope, + WebDriverBiDiJsonEnvelopeError, WebDriverBiDiReceivedTextMessage, +}; + +/// Typed protocol acknowledgment for one correlated WebDriver BiDi `session.end` command. +/// +/// WebDriver BiDi defines `session.EndResult` as the extensible `EmptyResult` object. The common +/// local-end envelope parser already validates the complete JSON document and requires a success +/// `result` object, so this command-specific boundary intentionally retains no generic result body +/// and accepts extension members. This value proves only that the remote end returned a correlated +/// protocol success; it does not prove Chromium process exit, profile deletion, resource release, +/// or any other OriginWeave operational teardown postcondition. +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub struct WebDriverBiDiSessionEndResult { + command_id: u64, +} + +impl WebDriverBiDiSessionEndResult { + /// Parse one bounded local-end message and consume its exact outstanding command on response. + /// + /// Complete JSON and common WebDriver BiDi envelope validation occur before correlation state + /// can be consumed. Successful responses retain only the matched command id. A correlatable + /// protocol-error response consumes its matching id and returns a typed remote failure, while + /// events, null-id errors, malformed envelopes, unknown ids, and command-kind mismatches fail + /// closed without consuming unrelated outstanding state. Only a sealed reply from the same + /// connection that registered the command can consume it; a replacement connection cannot + /// complete the request even when its session and command identifiers match. + pub fn parse_and_correlate( + message: &WebDriverBiDiReceivedTextMessage, + correlation: &mut WebDriverBiDiCommandCorrelation, + ) -> Result { + let envelope = WebDriverBiDiJsonEnvelope::parse(message.message()) + .map_err(|source| WebDriverBiDiSessionEndResponseError::Envelope { source })?; + let completed = correlation + .correlate_response_for_connection( + &envelope, + WebDriverBiDiCommandKind::SessionEnd, + message.connection_generation(), + ) + .map_err(|source| WebDriverBiDiSessionEndResponseError::Correlation { source })?; + + match completed.outcome() { + WebDriverBiDiCorrelatedResponseOutcome::Success => Ok(Self { + command_id: completed.command_id(), + }), + WebDriverBiDiCorrelatedResponseOutcome::Error => { + Err(WebDriverBiDiSessionEndResponseError::RemoteProtocolError { + command_id: completed.command_id(), + }) + } + } + } + + /// Return the exact local command identifier consumed by this protocol acknowledgment. + #[must_use] + pub const fn command_id(&self) -> u64 { + self.command_id + } +} + +/// Fail-closed failures while admitting one typed WebDriver BiDi `session.end` response. +#[derive(Debug)] +pub enum WebDriverBiDiSessionEndResponseError { + /// Common local-end JSON envelope validation failed before correlation state was touched. + Envelope { + /// Exact common-envelope validation failure. + source: WebDriverBiDiJsonEnvelopeError, + }, + /// Exact command-response correlation failed without consuming unrelated state. + Correlation { + /// Exact typed correlation failure. + source: WebDriverBiDiCommandCorrelationError, + }, + /// The remote end returned a correlatable WebDriver BiDi protocol error for this command. + RemoteProtocolError { + /// Exact local command identifier consumed by the protocol-error response. + command_id: u64, + }, +} + +impl fmt::Display for WebDriverBiDiSessionEndResponseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Envelope { .. } => { + formatter.write_str("WebDriver BiDi session.end envelope is invalid") + } + Self::Correlation { .. } => { + formatter.write_str("WebDriver BiDi session.end response correlation failed") + } + Self::RemoteProtocolError { .. } => { + formatter.write_str("WebDriver BiDi session.end returned a protocol error") + } + } + } +} + +impl Error for WebDriverBiDiSessionEndResponseError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + Self::Envelope { source } => Some(source), + Self::Correlation { source } => Some(source), + Self::RemoteProtocolError { .. } => None, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn response_errors_have_stable_messages_and_typed_sources() { + let envelope = WebDriverBiDiSessionEndResponseError::Envelope { + source: WebDriverBiDiJsonEnvelopeError::InvalidJson, + }; + assert_eq!( + envelope.to_string(), + "WebDriver BiDi session.end envelope is invalid" + ); + assert!(envelope.source().is_some()); + + let correlation = WebDriverBiDiSessionEndResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandNotOutstanding, + }; + assert_eq!( + correlation.to_string(), + "WebDriver BiDi session.end response correlation failed" + ); + assert!(correlation.source().is_some()); + + let remote = WebDriverBiDiSessionEndResponseError::RemoteProtocolError { command_id: 7 }; + assert_eq!( + remote.to_string(), + "WebDriver BiDi session.end returned a protocol error" + ); + assert!(remote.source().is_none()); + } +} diff --git a/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs new file mode 100644 index 000000000..610526556 --- /dev/null +++ b/crates/originweave-network/tests/webdriver_bidi_session_end_response.rs @@ -0,0 +1,288 @@ +use std::{ + error::Error, + io::{self, Read, Write}, + net::{TcpListener, TcpStream}, + thread, + time::Duration, +}; + +use originweave_core::WebDriverBiDiWebSocketEndpoint; +use originweave_network::{ + WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandCorrelationError, + WebDriverBiDiConnectionMessageRead, WebDriverBiDiReceivedTextMessage, + WebDriverBiDiSessionEndCommand, WebDriverBiDiSessionEndResponseError, + WebDriverBiDiSessionEndResult, WebDriverBiDiTcpConnectionPlan, WebDriverBiDiWebSocketClientKey, + WebDriverBiDiWebSocketHandshakePlan, WebDriverBiDiWebSocketMaskKey, + WebDriverBiDiWebSocketMessageReader, +}; + +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":{"vendorExtension":{"clean":true}}}"#; +const END_REMOTE_ERROR_RESPONSE: &[u8] = + br#"{"type":"error","id":7,"error":"unknown error","message":"remote refused"}"#; +const END_UNKNOWN_ID_RESPONSE: &[u8] = + br#"{"type":"success","id":8,"result":{"vendorExtension":true}}"#; +const END_MALFORMED_RESPONSE: &[u8] = br#"{"type":"success","id":7}"#; + +#[test] +fn unbound_end_command_cannot_consume_a_connection_bound_reply() -> Result<(), Box> { + use originweave_network::WebDriverBiDiCommandKind; + + let (message, mut original) = send_end_and_read_response(END_SUCCESS_RESPONSE)?; + let mut unbound = WebDriverBiDiCommandCorrelation::new(); + unbound.register_command_for(7, WebDriverBiDiCommandKind::SessionEnd)?; + assert!(matches!( + WebDriverBiDiSessionEndResult::parse_and_correlate(&message, &mut unbound), + Err(WebDriverBiDiSessionEndResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::CommandConnectionProvenanceMissing { + command_id: 7, + }, + }) + )); + assert_eq!(unbound.outstanding_count(), 1); + let result = WebDriverBiDiSessionEndResult::parse_and_correlate(&message, &mut original)?; + assert_eq!(result.command_id(), 7); + assert_eq!(original.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn event_and_null_id_error_preserve_the_sent_end_command() -> Result<(), Box> { + for (document, expected) in [ + ( + br#"{"type":"event","method":"log.entryAdded","params":{}}"#.as_slice(), + WebDriverBiDiCommandCorrelationError::EventIsNotResponse, + ), + ( + br#"{"type":"error","id":null,"error":"unknown error","message":"remote"}"#.as_slice(), + WebDriverBiDiCommandCorrelationError::UncorrelatableErrorResponse, + ), + ] { + let (message, mut correlation) = send_end_and_read_response(document)?; + assert!(matches!( + WebDriverBiDiSessionEndResult::parse_and_correlate(&message, &mut correlation), + Err(WebDriverBiDiSessionEndResponseError::Correlation { source }) if source == expected + )); + assert_eq!(correlation.outstanding_count(), 1); + } + Ok(()) +} + +#[test] +fn replacement_end_replies_preserve_original_pending_request_and_recovery() +-> Result<(), Box> { + for response in [END_SUCCESS_RESPONSE, END_REMOTE_ERROR_RESPONSE] { + let (original, mut pending) = send_end_and_read_response(END_SUCCESS_RESPONSE)?; + let (replacement, _) = send_end_and_read_response(response)?; + assert!(matches!( + WebDriverBiDiSessionEndResult::parse_and_correlate(&replacement, &mut pending), + Err(WebDriverBiDiSessionEndResponseError::Correlation { + source: WebDriverBiDiCommandCorrelationError::ResponseConnectionMismatch { + command_id: 7 + } + }) + )); + assert_eq!(pending.outstanding_count(), 1); + let result = WebDriverBiDiSessionEndResult::parse_and_correlate(&original, &mut pending)?; + assert_eq!(result.command_id(), 7); + assert_eq!(pending.outstanding_count(), 0); + } + Ok(()) +} + +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> { + 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 send_end_and_read_response( + response: &'static [u8], +) -> Result< + ( + WebDriverBiDiReceivedTextMessage, + WebDriverBiDiCommandCorrelation, + ), + Box, +> { + 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, response.len() as u8])?; + stream.write_all(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 text = match WebDriverBiDiWebSocketMessageReader::new(established) + .read_next(Duration::from_millis(500))? + { + WebDriverBiDiConnectionMessageRead::Text { message, .. } => message, + 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 response test server panicked"))??; + Ok((text, correlation)) +} + +#[test] +fn session_end_success_accepts_extensible_empty_result_and_consumes_exact_correlation() +-> Result<(), Box> { + let (text, mut correlation) = send_end_and_read_response(END_SUCCESS_RESPONSE)?; + assert_eq!(correlation.outstanding_count(), 1); + + let result = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation)?; + assert_eq!(result.command_id(), 7); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn session_end_remote_error_consumes_only_the_correlated_command() -> Result<(), Box> { + let (text, mut correlation) = send_end_and_read_response(END_REMOTE_ERROR_RESPONSE)?; + let parsed = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation); + let error = match parsed { + Ok(_) => { + return Err( + io::Error::other("remote error was accepted as session.end success").into(), + ); + } + Err(error) => error, + }; + + assert!(matches!( + error, + WebDriverBiDiSessionEndResponseError::RemoteProtocolError { command_id: 7 } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi session.end returned a protocol error" + ); + assert!(error.source().is_none()); + assert_eq!(correlation.outstanding_count(), 0); + Ok(()) +} + +#[test] +fn malformed_session_end_envelope_fails_before_consuming_correlation() -> Result<(), Box> +{ + let (text, mut correlation) = send_end_and_read_response(END_MALFORMED_RESPONSE)?; + let parsed = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation); + let error = match parsed { + Ok(_) => { + return Err(io::Error::other("malformed session.end response was accepted").into()); + } + Err(error) => error, + }; + + assert!(matches!( + error, + WebDriverBiDiSessionEndResponseError::Envelope { .. } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi session.end envelope is invalid" + ); + assert!(error.source().is_some()); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} + +#[test] +fn unknown_session_end_response_id_does_not_consume_the_outstanding_command() +-> Result<(), Box> { + let (text, mut correlation) = send_end_and_read_response(END_UNKNOWN_ID_RESPONSE)?; + let parsed = WebDriverBiDiSessionEndResult::parse_and_correlate(&text, &mut correlation); + let error = match parsed { + Ok(_) => { + return Err(io::Error::other("unknown session.end response id was accepted").into()); + } + Err(error) => error, + }; + + assert!(matches!( + error, + WebDriverBiDiSessionEndResponseError::Correlation { .. } + )); + assert_eq!( + error.to_string(), + "WebDriver BiDi session.end response correlation failed" + ); + assert!(error.source().is_some()); + assert_eq!(correlation.outstanding_count(), 1); + Ok(()) +} diff --git a/docs/doctoring.md b/docs/doctoring.md index ffe0f82ee..42a289dd1 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -128,12 +128,22 @@ On 6 September 2026, that historical no-connection-provenance state was supersed The #251 integration adopts #250 `ec433b844a121f8554c062f92267991af9cacb6f` by ordinary merge and retains both release records. Native discovery on predecessor `86e8ad76838f2a64aa7e0cd56ba1f931c8d0c3dc` collected zero command-correlation release tests; the current parent supplies the existing discoverable TestCase and synchronized opening-exchange fixtures. The session-end sender, its public exports and both child-owned Rust integration tests remain unchanged. The sender still registers only the exact typed command, retires correlation only after proven preflight rejection, and leaves ambiguous writes outstanding. A successful frame write does not prove session termination, browser-process exit, profile deletion, authenticated connection provenance or real Chromium acceptance. Local verification and current-head hosted checks remain separate prerequisites; earlier stack results do not transfer. +### Session-end response parent integration evidence + +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. + ### Current session-end sender adoption of status-reply provenance On 7 September 2026, the session-end sender stack reproduced the inherited status-reply gap at `edec535b8d8b3c843f9f9baf8562f31ff5c7af23`: two real loopback connections using the same session and command id allowed the replacement reply to complete the original pending status request. The ordinary merge of #250 `bbdc6ace7a5932adf24836700f806850e6b230bc` preserves the session-end sender and both original sender integration-test files byte-for-byte while inheriting sealed status receipts and connection-aware correlation. The regression now requires the exact connection-mismatch error, unchanged pending count, and subsequent completion using the original received reply. The server fixtures have already finished; this proves retained reply/correlation recovery, not liveness of an open original stream or same-endpoint replacement coverage. The shared routing validation, result projection order, and parent connection safeguards are unchanged. This supersedes the earlier no-received-provenance description for the status path only: the session-end sender still makes no received-acknowledgment, process-exit, profile-cleanup, browser-policy or release-acceptance claim. End-response provenance belongs to its separate consumer. Local tests and exact-head hosted checks must be evaluated independently after this adoption. +### Current session-end response connection binding + +On 7 September 2026, regression `6aaf7f3f` reproduced replacement-reply acceptance on #252. Ordinary adoption of #251 `924ad97551750d4a901ded38b89488cc5438e54f` retained the failure, confirming that parent status safeguards alone did not repair the end-response consumer. The end sender now uses the existing connection-bound registration before writing, and the response parser accepts the existing sealed received-text type and checks that connection through shared correlation before consuming pending state. The original sender preflight retirement and ambiguous-write behavior, result shape, command-id accessor and remote-error classification remain unchanged. No new authority accessor or duplicate reader is introduced. + +Real loopback tests require exact connection-mismatch rejection for replacement success and error replies, unchanged original pending state, and subsequent completion using the original received reply. They also reject unbound registrations, events and null-id errors without consuming the pending command. These fixtures use separate listener endpoints and retain receipts after their server threads finish; they do not prove same-endpoint replacement rejection or liveness of an open original connection. This supersedes the earlier unbound-response description for the current #252 branch only. A correlated acknowledgment still does not prove browser-process exit, profile deletion, resource cleanup, browser policy authority, protected integration or release acceptance. Exact-head local and hosted verification remain separate. + ## 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