Skip to content
Closed
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
4a7f46f
test(core): require typed browser operation capability binding
seonghobae Aug 12, 2026
c6d370e
style(core): format typed browser operation contract
seonghobae Aug 12, 2026
7a6f0e2
feat(core): derive adapter capability from typed browser operation
seonghobae Aug 12, 2026
4c4868f
style(core): format typed operation dispatch boundary
seonghobae Aug 13, 2026
9acbac7
docs(changelog): record typed browser operation binding
seonghobae Aug 13, 2026
751ae00
merge: align typed browser operation capabilities with current epoch …
seonghobae Aug 15, 2026
68fed65
merge: preserve browser lifetime authority in typed operation binding
seonghobae Aug 16, 2026
9935292
test: define buyer-visible browser operation vocabulary
seonghobae Aug 16, 2026
06635ab
feat(core): bind buyer-visible browser operation vocabulary
seonghobae Aug 16, 2026
d1d5c73
test(core): require bounded BiDi accessibility query
seonghobae Aug 16, 2026
341ccdc
style(core): apply canonical BiDi query test formatting
seonghobae Aug 16, 2026
ad3b5a2
feat(core): bound WebDriver BiDi accessibility query
seonghobae Aug 16, 2026
750348a
feat(core): export bounded BiDi accessibility query
seonghobae Aug 16, 2026
7fc113d
style(core): apply canonical BiDi query exports
seonghobae Aug 16, 2026
fd66444
test(core): require minimal BiDi node serialization
seonghobae Aug 16, 2026
75356e0
feat(core): minimize BiDi node serialization surface
seonghobae Aug 16, 2026
8a18184
feat(core): export minimal BiDi serialization limits
seonghobae Aug 16, 2026
b649787
test(bidi): reject over-budget locateNodes results
seonghobae Aug 16, 2026
7faa2c6
fix(bidi): revalidate locateNodes result budget
seonghobae Aug 16, 2026
d99181b
docs(changelog): record BiDi result budget revalidation
seonghobae Aug 16, 2026
b45c40b
test(bidi): require bounded remote node references
seonghobae Aug 16, 2026
64d7630
feat(core): admit bounded BiDi remote node references
cursoragent Aug 16, 2026
601628b
test(core): reject control-bearing BiDi locator text
cursoragent Aug 16, 2026
496f556
feat(core): reject control-bearing BiDi locator text
cursoragent Aug 16, 2026
9f38940
test(bidi): bind locateNodes results to current authority
cursoragent Aug 16, 2026
ce0af01
test(bidi): fix locateNodes admission test syntax
cursoragent Aug 16, 2026
5ee16f1
feat(core): bind locateNodes results to current authority
cursoragent Aug 16, 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
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Runtime browser-adapter version binding at the atomic protocol-use boundary: the caller-supplied bounded adapter-version token must exactly match the reviewed descriptor version before runtime revision or capability checks can succeed, preventing adapter-build drift from silently reusing otherwise matching protocol/browser metadata without authenticating or attesting the adapter process.
- Same-call browser-protocol dispatch gating that validates current protocol family, adapter version, pinned protocol/browser revisions, OriginWeave generation, and required capability before invoking one callback, transferring the non-cloneable validation proof by ownership without turning metadata validation into browser or Agent authority.
- Context-bound browser-protocol dispatch composition that revalidates the exact OriginWeave browser session/context pair, carries the registry's current document epoch into the immediate callback, and separately requires the same exact runtime protocol metadata/capability checks before dispatch without claiming origin, destination, typed-input, transport-authentication, or post-condition authority.
- Typed browser-protocol operations that derive the exact adapter capability inside the strongest current context/origin/document-epoch dispatch boundary, preventing callers from independently selecting mismatched operation and capability metadata without performing browser I/O or granting policy authority.
- Bounded WebDriver BiDi accessibility-node query parameters for `browsingContext.locateNodes`, with reviewed selector/result budgets, exact-token role and control-free accessible-name admission, minimal remote-value serialization options, and fail-closed revalidation that rejects an untrusted adapter response whose returned node count exceeds the exact request budget before node normalization or retention.
- Fail-closed WebDriver BiDi `script.NodeRemoteValue` admission that requires the exact remote type `node` and a non-empty `sharedId` within the same UTF-8 identifier budget as browser session and context identifiers, rejecting control and whitespace so an untrusted `locateNodes` item cannot be retained as a later typed-input handle without a usable shared node identity.
- Same-call `locateNodes` result admission that revalidates the exact current session, browsing context, canonical origin, and document epoch, rejects an over-budget or non-node result, and translates each admitted `sharedId` through the authority registry into an `ObservedNodeHandle` without performing browser I/O.
- 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.
- Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes.
Expand Down
499 changes: 499 additions & 0 deletions crates/originweave-core/src/browser_protocol_operation.rs

Large diffs are not rendered by default.

15 changes: 10 additions & 5 deletions crates/originweave-core/src/browser_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,7 +338,7 @@ impl Default for BrowserAuthorityRegistry {
/// A fail-closed error produced while translating external browser identifiers into local authority.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserRegistryError {
/// An external identifier was empty or exceeded the reviewed byte bound.
/// An external identifier was empty, contained control or whitespace, or exceeded the reviewed byte bound.
InvalidExternalIdentifier,
/// The supplied OriginWeave browser session is not registered in this registry.
UnknownBrowserSession,
Expand Down Expand Up @@ -366,9 +366,9 @@ pub enum BrowserRegistryError {
impl fmt::Display for BrowserRegistryError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidExternalIdentifier => {
formatter.write_str("external browser identifier must contain 1 to 512 UTF-8 bytes")
}
Self::InvalidExternalIdentifier => formatter.write_str(
"external browser identifier must contain 1 to 512 UTF-8 bytes without control or whitespace characters",
),
Self::UnknownBrowserSession => {
formatter.write_str("browser session is not registered in this authority registry")
}
Expand Down Expand Up @@ -402,7 +402,12 @@ impl fmt::Display for BrowserRegistryError {
impl std::error::Error for BrowserRegistryError {}

fn validate_external_identifier(identifier: &str) -> Result<(), BrowserRegistryError> {
if identifier.is_empty() || identifier.len() > MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES {
if identifier.is_empty()
|| identifier.len() > MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES
|| identifier
.chars()
.any(|character| character.is_control() || character.is_whitespace())
{
return Err(BrowserRegistryError::InvalidExternalIdentifier);
}
Ok(())
Expand Down
10 changes: 10 additions & 0 deletions crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@

mod browser_protocol;
mod browser_protocol_dispatch;
mod browser_protocol_operation;
mod browser_registry;
#[cfg(test)]
mod browser_registry_coverage;
Expand All @@ -27,6 +28,15 @@ pub use browser_protocol_dispatch::{
BrowserContextOriginEpochDispatchTarget, BrowserContextProtocolDispatchError,
BrowserProtocolRuntimeMetadata,
};
pub use browser_protocol_operation::{
BrowserProtocolOperation, MAX_BROWSER_ACCESSIBILITY_QUERY_NAME_BYTES,
MAX_BROWSER_ACCESSIBILITY_QUERY_NODE_COUNT, MAX_BROWSER_ACCESSIBILITY_QUERY_ROLE_BYTES,
WEBDRIVER_BIDI_LOCATE_NODES_METHOD, WEBDRIVER_BIDI_NODE_REMOTE_VALUE_TYPE,
WEBDRIVER_BIDI_QUERY_INCLUDE_SHADOW_TREE, WEBDRIVER_BIDI_QUERY_MAX_DOM_DEPTH,
WEBDRIVER_BIDI_QUERY_MAX_OBJECT_DEPTH, WebDriverBiDiAccessibilityQuery,
WebDriverBiDiAccessibilityQueryError, WebDriverBiDiLocateNodesAdmissionError,
WebDriverBiDiRemoteNodeReference, WebDriverBiDiRemoteNodeReferenceError,
};
pub use browser_registry::{
BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES,
};
Expand Down
23 changes: 22 additions & 1 deletion crates/originweave-core/tests/browser_authority_registry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ fn public_default_and_error_contracts_are_usable_from_an_adapter() -> Result<(),
let cases = [
(
BrowserRegistryError::InvalidExternalIdentifier,
"external browser identifier must contain 1 to 512 UTF-8 bytes".to_owned(),
"external browser identifier must contain 1 to 512 UTF-8 bytes without control or whitespace characters".to_owned(),
),
(
BrowserRegistryError::UnknownBrowserSession,
Expand Down Expand Up @@ -214,6 +214,27 @@ fn external_identifiers_are_bounded_without_assuming_protocol_syntax() -> Result

let unicode = registry.register_session("세션-opaque-✓")?;
assert!(unicode.value() > 0);

assert_eq!(
registry.register_session(" "),
Err(BrowserRegistryError::InvalidExternalIdentifier)
);
assert_eq!(
registry.register_session("webdriver-session\n"),
Err(BrowserRegistryError::InvalidExternalIdentifier)
);
assert_eq!(
registry.register_session("webdriver-session\u{0000}"),
Err(BrowserRegistryError::InvalidExternalIdentifier)
);

let session = registry.register_session("webdriver-session")?;
let context = registry.register_context(session, "top-level-context")?;
let origin = loopback_origin();
assert_eq!(
registry.bind_node(session, context, &origin, "backend-node-17\n"),
Err(BrowserRegistryError::InvalidExternalIdentifier)
);
Ok(())
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
use std::{cell::Cell, error::Error, io};

use originweave_core::{
BrowserAuthorityRegistry, BrowserContextDispatchTarget, BrowserContextOriginDispatchTarget,
BrowserContextOriginEpochDispatchTarget, BrowserContextProtocolDispatchError,
BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolKind,
BrowserProtocolOperation, BrowserProtocolRuntimeMetadata, DocumentEpoch, Origin,
OriginWeaveProtocolVersion, ValidatedBrowserProtocolUse,
};

const ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion =
OriginWeaveProtocolVersion::new(0, 1);
const ADAPTER_VERSION: &str = "originweave-bidi-v1";
const PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01";
const BROWSER_REVISION: &str = "chromium-r1639810";

fn descriptor(
capabilities: &[BrowserProtocolCapability],
) -> Result<BrowserProtocolAdapterDescriptor, Box<dyn Error>> {
Ok(BrowserProtocolAdapterDescriptor::new(
BrowserProtocolKind::WebDriverBiDi,
ORIGINWEAVE_PROTOCOL_VERSION,
ADAPTER_VERSION,
PROTOCOL_REVISION,
BROWSER_REVISION,
capabilities,
)?)
}

fn runtime_metadata() -> BrowserProtocolRuntimeMetadata<'static> {
BrowserProtocolRuntimeMetadata::new(
BrowserProtocolKind::WebDriverBiDi,
ADAPTER_VERSION,
PROTOCOL_REVISION,
BROWSER_REVISION,
)
}

fn origin(value: &str) -> Result<Origin, Box<dyn Error>> {
Origin::parse(value).map_err(|_| {
Box::new(io::Error::new(
io::ErrorKind::InvalidInput,
"invalid controlled origin fixture",
)) as Box<dyn Error>
})
}

fn typed_input_target<'a>(
registry: &mut BrowserAuthorityRegistry,
expected_origin: &'a Origin,
) -> Result<BrowserContextOriginEpochDispatchTarget<'a>, Box<dyn Error>> {
let session = registry.register_session("webdriver-session")?;
let context = registry.register_context(session, "top-level-context")?;
let epoch = registry.bind_context_origin(session, context, expected_origin)?;
Ok(BrowserContextOriginEpochDispatchTarget::new(
BrowserContextOriginDispatchTarget::new(
BrowserContextDispatchTarget::new(session, context),
expected_origin,
),
epoch,
))
}

#[test]
fn buyer_visible_operations_map_to_exact_transport_capabilities() {
let expected = [
(
BrowserProtocolOperation::Navigate,
BrowserProtocolCapability::Navigation,
),
(
BrowserProtocolOperation::QueryNodes,
BrowserProtocolCapability::SemanticObservation,
),
(
BrowserProtocolOperation::ClickNode,
BrowserProtocolCapability::TypedInput,
),
(
BrowserProtocolOperation::TypeText,
BrowserProtocolCapability::TypedInput,
),
(
BrowserProtocolOperation::WaitForState,
BrowserProtocolCapability::SemanticObservation,
),
(
BrowserProtocolOperation::ObserveNetwork,
BrowserProtocolCapability::NetworkObservation,
),
];

for (operation, capability) in expected {
assert_eq!(operation.required_capability(), capability);
}
}

#[test]
fn typed_operation_dispatch_derives_the_required_capability() -> Result<(), Box<dyn Error>> {
let descriptor = descriptor(&[BrowserProtocolCapability::TypedInput])?;
let mut registry = BrowserAuthorityRegistry::new();
let expected_origin = origin("https://app.example")?;
let target = typed_input_target(&mut registry, &expected_origin)?;

let result = descriptor.dispatch_operation_if_context_origin_epoch_current(
&registry,
target,
ORIGINWEAVE_PROTOCOL_VERSION,
runtime_metadata(),
BrowserProtocolOperation::TypeText,
|validated: ValidatedBrowserProtocolUse, operation, epoch: DocumentEpoch| {
(operation, validated.capability(), epoch.value())
},
)?;

assert_eq!(
result,
(
BrowserProtocolOperation::TypeText,
BrowserProtocolCapability::TypedInput,
1,
)
);
Ok(())
}

#[test]
fn unsupported_typed_operation_fails_before_dispatch_callback() -> Result<(), Box<dyn Error>> {
let descriptor = descriptor(&[BrowserProtocolCapability::SemanticObservation])?;
let mut registry = BrowserAuthorityRegistry::new();
let expected_origin = origin("https://app.example")?;
let target = typed_input_target(&mut registry, &expected_origin)?;
let dispatch_called = Cell::new(false);

let result = descriptor.dispatch_operation_if_context_origin_epoch_current(
&registry,
target,
ORIGINWEAVE_PROTOCOL_VERSION,
runtime_metadata(),
BrowserProtocolOperation::ClickNode,
|_validated, _operation, _epoch| dispatch_called.set(true),
);

assert!(matches!(
result,
Err(BrowserContextProtocolDispatchError::ProtocolValidation(_))
));
assert!(!dispatch_called.get());
Ok(())
}
Loading
Loading