Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
68 commits
Select commit Hold shift + click to select a range
160365b
test(network): require correlated text-value postcondition admission
seonghobae Sep 1, 2026
924e027
feat(network): admit correlated text-value postconditions
seonghobae Sep 1, 2026
384643f
feat(network): export typed text-value response admission
seonghobae Sep 1, 2026
cecc0ac
style(network): apply rustfmt to text-value response admission
seonghobae Sep 1, 2026
724e323
test(network): use valid JSON escape fixture for text observation
seonghobae Sep 1, 2026
8861c03
test(network): keep projection regression clippy-clean
seonghobae Sep 1, 2026
d897909
fix(network): close BiDi response coverage gaps
seonghobae Sep 1, 2026
8357e9e
test(network): exercise BiDi response failure paths
seonghobae Sep 1, 2026
9a27068
style(network): apply canonical rustfmt diagnostics
seonghobae Sep 1, 2026
11d6588
style(network): apply remaining rustfmt diagnostics
seonghobae Sep 1, 2026
8c09f1a
test(network): cover observation error correlation
seonghobae Sep 1, 2026
0ba5a4d
test(network): cover expected text rejection paths
seonghobae Sep 1, 2026
57af3c8
style(network): apply canonical rustfmt
seonghobae Sep 1, 2026
67d9046
test(network): close typed-text coverage obligations
seonghobae Sep 1, 2026
26b455d
test(network): cover malformed text observation paths
seonghobae Sep 1, 2026
edc4a7b
test(network): replace non-causal coverage probes
seonghobae Sep 1, 2026
f8ed02a
test(network): cover malformed observation propagation
seonghobae Sep 1, 2026
64c8588
test(network): close exact observation coverage
seonghobae Sep 1, 2026
14ab832
test(network): close observation coverage instrumentation
seonghobae Sep 1, 2026
3c2135c
test(network): satisfy coverage and strict clippy together
seonghobae Sep 1, 2026
1f2d298
fix(network): keep text validation assertion type-local
seonghobae Sep 1, 2026
086d11b
test(network): cover public text observation diagnostics
seonghobae Sep 1, 2026
8d38457
style(network): restore rustfmt newline
seonghobae Sep 1, 2026
364f2f2
test(network): cover unicode observation projection
seonghobae Sep 1, 2026
12656b1
style(network): apply canonical rustfmt to unicode response tests
seonghobae Sep 1, 2026
aa732cb
fix(network): restore exact unicode JSON fixture bytes
seonghobae Sep 1, 2026
a8d7a0a
test(network): exercise text-value public boundary in unit build
seonghobae Sep 1, 2026
9903195
test(network): run text-value boundary in unit build
seonghobae Sep 1, 2026
13afb69
test(network): assert exact page text stays redacted
seonghobae Sep 1, 2026
34e4980
test(network): exercise fail-closed text response paths
seonghobae Sep 1, 2026
eb05dd6
style(network): apply exact current-head rustfmt diagnostics
seonghobae Sep 1, 2026
1bdd8ae
test(network): cover typed text error display adapters
seonghobae Sep 1, 2026
d7c9878
test(network): require explicit text postcondition gate
seonghobae Sep 1, 2026
dce81cb
feat(network): require verified text postcondition
seonghobae Sep 1, 2026
aaec533
feat(network): expose verified text postcondition
seonghobae Sep 1, 2026
6d5559a
test(network): preserve postcondition error for diagnostics
seonghobae Sep 1, 2026
58af9ed
test(network): cover postcondition error contracts
seonghobae Sep 1, 2026
48aeba9
fix(network): apply canonical rustfmt to postcondition tests
seonghobae Sep 1, 2026
61bd2e0
fix(network): avoid prohibited expect in postcondition tests
seonghobae Sep 1, 2026
335667e
test(network): reject unbound postcondition response ids
seonghobae Sep 1, 2026
ed1e146
chore(stack): inherit typed observation correlation provenance
seonghobae Sep 1, 2026
b7e1c7b
chore(stack): inherit typed observation registration
seonghobae Sep 1, 2026
f67c8bf
chore(stack): inherit typed command provenance regression
seonghobae Sep 1, 2026
426f9dc
fix(stack): reconcile #271 with current #270 command provenance
seonghobae Sep 2, 2026
89e19ec
fix(network): bind text observations to command kind
seonghobae Sep 4, 2026
90f4620
Merge remote-tracking branch 'origin/feat/webdriver-bidi-type-text-po…
seonghobae Sep 4, 2026
bc8d9b1
merge: adopt observation transport parent
seonghobae Sep 5, 2026
802ec80
docs(network): scope typed-text postconditions
seonghobae Sep 5, 2026
2531887
test(network): reject text observation replies from another connection
seonghobae Sep 7, 2026
17d1603
test(network): use existing sibling family in response regression
seonghobae Sep 7, 2026
29cd0d6
test(network): preserve an unrelated observation request in regression
seonghobae Sep 7, 2026
14efb67
fix(network): adopt current observation transport parent
seonghobae Sep 7, 2026
db0fa25
fix(network): bind field value replies to their sender connection
seonghobae Sep 7, 2026
b0410ae
test(network): prove original observation survives replacement reply
seonghobae Sep 7, 2026
d9a3a40
test(network): expose typed-input intent substitution
seonghobae Sep 7, 2026
af1defe
fix(network): bind text postcondition to dispatched intent
seonghobae Sep 7, 2026
1e600c4
test(network): reject cross-connection postcondition evidence
seonghobae Sep 7, 2026
e405f9f
fix(network): retain typed-input connection generation
seonghobae Sep 7, 2026
a73ea7b
fix(network): reject foreign postcondition connection
seonghobae Sep 7, 2026
570f166
style(network): apply canonical rustfmt to postcondition tests
seonghobae Sep 7, 2026
c2c0f4f
test(network): assert typed foreign-connection rejection
seonghobae Sep 7, 2026
a31971b
style(network): apply canonical rustfmt to type-text intent
seonghobae Sep 7, 2026
6b67fe4
test(network): preserve connection in postcondition fixtures
seonghobae Sep 7, 2026
952d912
test(network): keep positive postconditions on one connection
seonghobae Sep 7, 2026
8c57193
test(network): isolate value and connection provenance cases
seonghobae Sep 7, 2026
33731d0
test(network): keep combined fixture response in short-frame budget
seonghobae Sep 7, 2026
14fb8e7
test(network): keep unrelated response on public typed transport
seonghobae Sep 7, 2026
46db004
fix(network): complete intent acknowledgement verification
seonghobae Sep 8, 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 AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,8 @@ The organization currently documents a **solo-maintainer** governance condition.

## Testing expectations

For an ACK coverage deficit, trace every pre-consumption guard and the lower correlation result before adding tests. Remove a duplicate post-check only when the same validated envelope and exact-id correlation make its failure impossible. Cover reachable malformed, event, remote-error, wrong-id and foreign-connection paths with real receipts, assert pending-state counts, and compare opaque diagnostics without exposing text. Keep reusable test helpers public only within integration-test crates; never widen production provenance access to make a fixture compile.

Use realistic cases, including:

- malformed origins, IPv4/IPv6 loopback, user information, paths, ports, and Unicode/control input;
Expand Down
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ All notable changes to OriginWeave are documented in this file. The format follo

## [Unreleased]

- Strengthened text-entry acknowledgment checks with real-connection regression tests for unrelated replies, malformed responses and private diagnostics; removed a redundant check without changing pending-request protection.

- Reject field-value replies received on a replacement connection, even when their request identifier and text match.

- 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.
Expand Down Expand Up @@ -97,6 +101,7 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Registry-issued admitted node handles and authority-bound WebDriver BiDi pointer-click construction that revalidate the exact session, context, canonical origin, document epoch, registry provenance, and retained `sharedId` before serializing `input.performActions`; caller-constructed node tuples or arbitrary wire identifiers cannot become typed-input authority, and the command itself grants no policy or Agent authority.
- Node-bound WebDriver BiDi text input that revalidates the exact session, browsing context, canonical origin, current document epoch, registry-issued node provenance, and admitted `sharedId` before serializing bounded protocol-safe non-secret `input.performActions`; the command focuses the admitted element before keyboard input, and its diagnostic representation exposes only command metadata and text byte length rather than typed text or the serialized wire payload.
- Fixed sandboxed text-value observation for an admitted current node, with product-owned `script.callFunction` source and no generic script surface; transport correlation and observed post-condition comparison remain separate active-stack work.
- Typed text-value post-condition verification that admits only the exact correlated observation response, compares it with the already-authorized expected text, discards page-controlled text, and reports mismatch without treating command acknowledgement as success.
- Fail-closed rejection of reviewed Unicode format and bidirectional-override characters in accessibility roles, accessible names, BiDi `sharedId` values, and registry external identifiers, while ordinary spaces in accessible names remain valid.
- Credential-safe browser-protocol validation evidence that copies only the already validated protocol family, OriginWeave generation, adapter version, pinned protocol/browser revisions, and exact capability into cloneable audit metadata without recreating the non-cloneable validation prerequisite or granting browser/Agent authority.
- Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors.
Expand Down
23 changes: 22 additions & 1 deletion crates/originweave-network/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
//! 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 and fixed sandboxed text-value observations, 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, binds positive text-value evidence to a sender-minted
//! acknowledged typed-input intent, 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,7 +52,10 @@ 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_response;
mod webdriver_bidi_text_value_observation_transport;
mod webdriver_bidi_text_value_postcondition;
mod webdriver_bidi_type_text_intent;
mod webdriver_bidi_type_text_response;
mod webdriver_bidi_type_text_transport;
mod webdriver_bidi_websocket_frame;
Expand All @@ -60,6 +66,8 @@ mod webdriver_bidi_websocket_transport_closure;

#[cfg(test)]
mod webdriver_bidi_json_envelope_public_boundary_tests;
#[cfg(test)]
mod webdriver_bidi_text_value_observation_public_boundary_tests;

pub use connection::{
ConnectionPlan, DirectTcpConnection, MAX_CONNECT_TIMEOUT, MAX_CONNECTION_ATTEMPTS,
Expand Down Expand Up @@ -147,9 +155,22 @@ pub use webdriver_bidi_session_teardown::{
WebDriverBiDiSessionTeardownAssessment, WebDriverBiDiSessionTeardownAssessmentError,
WebDriverBiDiSessionTeardownDisposition, WebDriverBiDiSessionTeardownObservations,
};
pub use webdriver_bidi_text_value_observation_response::{
WebDriverBiDiTextValueObservationProjectionError,
WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult,
};
pub use webdriver_bidi_text_value_observation_transport::{
WebDriverBiDiTextValueObservationSendError, send_webdriver_bidi_text_value_observation,
};
pub use webdriver_bidi_text_value_postcondition::{
WebDriverBiDiTextValuePostcondition, WebDriverBiDiTextValuePostconditionError,
verify_webdriver_bidi_text_value_postcondition,
};
pub use webdriver_bidi_type_text_intent::{
WebDriverBiDiAcknowledgedTypeTextIntent, WebDriverBiDiTypeTextIntentAcknowledgementError,
WebDriverBiDiTypeTextIntentWitness, acknowledge_webdriver_bidi_type_text_intent,
send_webdriver_bidi_type_text_with_postcondition_intent,
};
pub use webdriver_bidi_type_text_response::{
WebDriverBiDiTypeTextResponseError, WebDriverBiDiTypeTextResult,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
use std::{
error::Error,
io::{self, Read, Write},
net::{TcpListener, TcpStream},
thread,
time::Duration,
};

use originweave_core::WebDriverBiDiWebSocketEndpoint;

use crate::{
WebDriverBiDiCommandCorrelation, WebDriverBiDiCommandKind, WebDriverBiDiConnectionMessageRead,
WebDriverBiDiReceivedTextMessage, WebDriverBiDiTcpConnectionPlan,
WebDriverBiDiTextValueObservationResponseError, WebDriverBiDiTextValueObservationResult,
WebDriverBiDiWebSocketClientKey, WebDriverBiDiWebSocketHandshakePlan,
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 ERROR_UNKNOWN_COMMAND: &[u8] =
br#"{"type":"error","id":71,"error":"unknown error","message":"remote failure"}"#;
const MALFORMED_PROJECTION: &[u8] = br#"{"type":"success","id":70,"result":{"type":"success","result":{"type":"string","value":"expected"}}}"#;
const SUCCESS_UNKNOWN_COMMAND: &[u8] = br#"{"type":"success","id":72,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"expected"}}}"#;
const FINAL_EXPECTED_TEXT: &str = "Quarterly review";
const VALID_SUCCESS: &[u8] = br#"{"type":"success","id":70,"result":{"type":"success","realm":"realm-1","result":{"type":"string","value":"Quarterly review"}}}"#;

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 write_unmasked_text_frame(stream: &mut TcpStream, document: &[u8]) -> io::Result<()> {
if document.len() <= 125 {
let length = u8::try_from(document.len()).map_err(|_| {
io::Error::new(io::ErrorKind::InvalidData, "short frame length exceeds u8")
})?;
stream.write_all(&[0x81, length])?;
} else {
let length = u16::try_from(document.len()).map_err(|_| {
io::Error::new(
io::ErrorKind::InvalidData,
"unit JSON document exceeds two-byte frame length",
)
})?;
stream.write_all(&[0x81, 126])?;
stream.write_all(&length.to_be_bytes())?;
}
stream.write_all(document)
}

fn read_text_over_loopback(
document: &'static [u8],
correlation: Option<&mut WebDriverBiDiCommandCorrelation>,
) -> Result<WebDriverBiDiReceivedTextMessage, 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)?;
write_unmasked_text_frame(&mut stream, document)
});

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 established = WebDriverBiDiWebSocketHandshakePlan::new(
connection,
WebDriverBiDiWebSocketClientKey::new(RFC6455_SAMPLE_KEY)?,
)?
.write_opening_request(Duration::from_millis(500))?
.read_opening_response(Duration::from_millis(500))?;
if let Some(correlation) = correlation {
correlation.register_command_for_connection(
70,
WebDriverBiDiCommandKind::TextValueObservation,
established.transport_evidence().connection_generation(),
)?;
}
let text = match WebDriverBiDiWebSocketMessageReader::new(established)
.read_next(Duration::from_millis(500))?
{
WebDriverBiDiConnectionMessageRead::Text { message, .. } => message,
other => {
return Err(io::Error::other(format!(
"validated text frame produced unexpected assembly state: {other:?}"
))
.into());
}
};

server
.join()
.map_err(|_| io::Error::other("text-value unit server panicked"))??;
Ok(text)
}

#[test]
fn public_text_value_boundary_covers_error_adapters_and_credential_safe_result()
-> Result<(), Box<dyn Error>> {
let mut correlation = WebDriverBiDiCommandCorrelation::new();
correlation.register_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?;

let invalid = read_text_over_loopback(b"not-json", None)?;
let envelope_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare(
&invalid,
"expected",
&mut correlation,
);
assert!(matches!(
&envelope_result,
Err(WebDriverBiDiTextValueObservationResponseError::Envelope { .. })
));
assert_eq!(
envelope_result
.as_ref()
.err()
.map(ToString::to_string)
.as_deref(),
Some("WebDriver BiDi text-value observation envelope is invalid")
);
assert_eq!(correlation.outstanding_count(), 1);

let error_unknown = read_text_over_loopback(ERROR_UNKNOWN_COMMAND, None)?;
assert!(matches!(
WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare(
&error_unknown,
"expected",
&mut correlation,
),
Err(WebDriverBiDiTextValueObservationResponseError::Correlation { .. })
));
assert_eq!(correlation.outstanding_count(), 1);

let malformed_projection = read_text_over_loopback(MALFORMED_PROJECTION, None)?;
let projection_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare(
&malformed_projection,
"expected",
&mut correlation,
);
assert!(matches!(
&projection_result,
Err(WebDriverBiDiTextValueObservationResponseError::Projection { .. })
));
assert_eq!(
projection_result
.as_ref()
.err()
.map(ToString::to_string)
.as_deref(),
Some("WebDriver BiDi text-value observation result is invalid")
);
assert_eq!(correlation.outstanding_count(), 1);

let success_unknown = read_text_over_loopback(SUCCESS_UNKNOWN_COMMAND, None)?;
let correlation_result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare(
&success_unknown,
"expected",
&mut correlation,
);
assert!(matches!(
&correlation_result,
Err(WebDriverBiDiTextValueObservationResponseError::Correlation { .. })
));
assert_eq!(
correlation_result
.as_ref()
.err()
.map(ToString::to_string)
.as_deref(),
Some("WebDriver BiDi text-value observation response correlation failed")
);
assert_eq!(correlation.outstanding_count(), 1);

correlation.retire_command_for(70, WebDriverBiDiCommandKind::TextValueObservation)?;
let valid_success = read_text_over_loopback(VALID_SUCCESS, Some(&mut correlation))?;
let result = WebDriverBiDiTextValueObservationResult::parse_correlate_and_compare(
&valid_success,
FINAL_EXPECTED_TEXT,
&mut correlation,
)?;
assert_eq!(result.command_id(), 70);
assert_eq!(result.observed_text_bytes(), FINAL_EXPECTED_TEXT.len());
assert!(result.matches_expected_text());
assert_eq!(correlation.outstanding_count(), 0);

let debug = format!("{result:?}");
assert!(debug.contains("WebDriverBiDiTextValueObservationResult"));
assert!(!debug.contains(FINAL_EXPECTED_TEXT));
Ok(())
}
Loading
Loading