From cd784b660a0268cc95725d847bf895edb4b3d5fb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 05:38:17 +0900 Subject: [PATCH 1/3] test(core): require node-to-business-action binding --- .../tests/semantic_node_action_binding.rs | 134 ++++++++++++++++++ 1 file changed, 134 insertions(+) create mode 100644 crates/originweave-core/tests/semantic_node_action_binding.rs diff --git a/crates/originweave-core/tests/semantic_node_action_binding.rs b/crates/originweave-core/tests/semantic_node_action_binding.rs new file mode 100644 index 00000000..4b326f34 --- /dev/null +++ b/crates/originweave-core/tests/semantic_node_action_binding.rs @@ -0,0 +1,134 @@ +use std::collections::BTreeSet; + +use originweave_core::{ + ActionIntentDigest, ActionKind, ActionRequest, BrowserSessionId, BrowsingContextId, + DocumentEpoch, InstructionSource, NodeActionKind, NodeHandleError, ObservationChannel, + ObservedNodeHandle, Origin, SecretDelivery, SemanticNodeActionBinding, + SemanticNodeActionBindingError, SemanticNodeActionTarget, SemanticNodeObservation, + SemanticNodeObservationInput, +}; + +const VALID_INTENT: &str = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +fn origin(value: &str) -> Result { + Origin::parse(value).map_err(|error| format!("{error:?}")) +} + +fn observation() -> Result { + let handle = ObservedNodeHandle::new( + BrowserSessionId::new(7).map_err(|error| error.to_string())?, + BrowsingContextId::new(11).map_err(|error| error.to_string())?, + origin("https://app.example")?, + DocumentEpoch::new(3).map_err(|error| error.to_string())?, + 17, + ) + .map_err(|error| error.to_string())?; + + SemanticNodeObservation::new(SemanticNodeObservationInput { + handle, + parent: None, + children: Vec::new(), + role: "button".to_owned(), + accessible_name: "Continue".to_owned(), + visible_text: Some("Continue".to_owned()), + enabled: true, + visible: true, + selected: None, + supported_actions: BTreeSet::from([NodeActionKind::Click]), + evidence_channels: BTreeSet::from([ObservationChannel::Accessibility]), + }) + .map_err(|error| error.to_string()) +} + +fn action_request(source: Origin, target: Origin) -> Result { + let intent = ActionIntentDigest::parse(VALID_INTENT).map_err(|error| format!("{error:?}"))?; + Ok(ActionRequest::new( + ActionKind::Navigate, + source, + target, + InstructionSource::User, + SecretDelivery::None, + intent, + )) +} + +#[test] +fn node_action_binding_preserves_node_target_and_business_request() -> Result<(), String> { + let observed = observation()?; + let target = SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click) + .map_err(|error| error.to_string())?; + let request = action_request(origin("https://app.example")?, origin("https://next.example")?)?; + + let binding = SemanticNodeActionBinding::new(target.clone(), request.clone()) + .map_err(|error| error.to_string())?; + + assert_eq!(binding.target(), &target); + assert_eq!(binding.request(), &request); + Ok(()) +} + +#[test] +fn node_action_binding_rejects_request_from_another_document_origin() -> Result<(), String> { + let observed = observation()?; + let target = SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click) + .map_err(|error| error.to_string())?; + let request = action_request(origin("https://other.example")?, origin("https://next.example")?)?; + + assert_eq!( + SemanticNodeActionBinding::new(target, request).err(), + Some(SemanticNodeActionBindingError::SourceOriginMismatch) + ); + Ok(()) +} + +#[test] +fn node_action_binding_does_not_conflate_source_node_with_navigation_target() -> Result<(), String> { + let observed = observation()?; + let target = SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click) + .map_err(|error| error.to_string())?; + let destination = origin("https://destination.example")?; + let request = action_request(origin("https://app.example")?, destination.clone())?; + + let binding = SemanticNodeActionBinding::new(target, request) + .map_err(|error| error.to_string())?; + + assert_eq!(binding.request().target_origin(), &destination); + Ok(()) +} + +#[test] +fn node_action_binding_revalidates_exact_browser_authority_before_dispatch() -> Result<(), String> { + let observed = observation()?; + let target = SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click) + .map_err(|error| error.to_string())?; + let request = action_request(origin("https://app.example")?, origin("https://next.example")?)?; + let binding = SemanticNodeActionBinding::new(target, request) + .map_err(|error| error.to_string())?; + let observed_epoch = DocumentEpoch::new(3).map_err(|error| error.to_string())?; + let current_epoch = DocumentEpoch::new(4).map_err(|error| error.to_string())?; + + assert_eq!( + binding + .validate_current( + BrowserSessionId::new(7).map_err(|error| error.to_string())?, + BrowsingContextId::new(11).map_err(|error| error.to_string())?, + &origin("https://app.example")?, + current_epoch, + ) + .err(), + Some(NodeHandleError::StaleDocumentEpoch { + observed: observed_epoch, + current: current_epoch, + }) + ); + Ok(()) +} + +#[test] +fn node_action_binding_error_is_stable_and_credential_free() { + assert_eq!( + SemanticNodeActionBindingError::SourceOriginMismatch.to_string(), + "semantic node origin does not match action request source origin" + ); +} From 33fd6ae933e1c5de36058e3adb28ea3d88905f06 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 05:44:29 +0900 Subject: [PATCH 2/3] style(core): apply canonical action-binding rustfmt --- .../tests/semantic_node_action_binding.rs | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_action_binding.rs b/crates/originweave-core/tests/semantic_node_action_binding.rs index 4b326f34..f6b3ef18 100644 --- a/crates/originweave-core/tests/semantic_node_action_binding.rs +++ b/crates/originweave-core/tests/semantic_node_action_binding.rs @@ -58,7 +58,10 @@ fn node_action_binding_preserves_node_target_and_business_request() -> Result<() let observed = observation()?; let target = SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click) .map_err(|error| error.to_string())?; - let request = action_request(origin("https://app.example")?, origin("https://next.example")?)?; + let request = action_request( + origin("https://app.example")?, + origin("https://next.example")?, + )?; let binding = SemanticNodeActionBinding::new(target.clone(), request.clone()) .map_err(|error| error.to_string())?; @@ -73,7 +76,10 @@ fn node_action_binding_rejects_request_from_another_document_origin() -> Result< let observed = observation()?; let target = SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click) .map_err(|error| error.to_string())?; - let request = action_request(origin("https://other.example")?, origin("https://next.example")?)?; + let request = action_request( + origin("https://other.example")?, + origin("https://next.example")?, + )?; assert_eq!( SemanticNodeActionBinding::new(target, request).err(), @@ -83,15 +89,16 @@ fn node_action_binding_rejects_request_from_another_document_origin() -> Result< } #[test] -fn node_action_binding_does_not_conflate_source_node_with_navigation_target() -> Result<(), String> { +fn node_action_binding_does_not_conflate_source_node_with_navigation_target() -> Result<(), String> +{ let observed = observation()?; let target = SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click) .map_err(|error| error.to_string())?; let destination = origin("https://destination.example")?; let request = action_request(origin("https://app.example")?, destination.clone())?; - let binding = SemanticNodeActionBinding::new(target, request) - .map_err(|error| error.to_string())?; + let binding = + SemanticNodeActionBinding::new(target, request).map_err(|error| error.to_string())?; assert_eq!(binding.request().target_origin(), &destination); Ok(()) @@ -102,9 +109,12 @@ fn node_action_binding_revalidates_exact_browser_authority_before_dispatch() -> let observed = observation()?; let target = SemanticNodeActionTarget::from_observation(&observed, NodeActionKind::Click) .map_err(|error| error.to_string())?; - let request = action_request(origin("https://app.example")?, origin("https://next.example")?)?; - let binding = SemanticNodeActionBinding::new(target, request) - .map_err(|error| error.to_string())?; + let request = action_request( + origin("https://app.example")?, + origin("https://next.example")?, + )?; + let binding = + SemanticNodeActionBinding::new(target, request).map_err(|error| error.to_string())?; let observed_epoch = DocumentEpoch::new(3).map_err(|error| error.to_string())?; let current_epoch = DocumentEpoch::new(4).map_err(|error| error.to_string())?; From c150e2daa0c890c8e2797ebb4c88a6220be13019 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 05:49:47 +0900 Subject: [PATCH 3/3] feat(core): bind semantic node targets to business actions --- crates/originweave-core/src/lib.rs | 2 + .../src/semantic_action_binding.rs | 76 +++++++++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 crates/originweave-core/src/semantic_action_binding.rs diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index e604c899..b7040f2c 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -11,6 +11,7 @@ mod browser_registry; #[cfg(test)] mod browser_registry_coverage; mod contracts; +mod semantic_action_binding; mod semantic_action_target; mod semantic_observation; @@ -18,6 +19,7 @@ pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, }; pub use contracts::*; +pub use semantic_action_binding::{SemanticNodeActionBinding, SemanticNodeActionBindingError}; pub use semantic_action_target::{SemanticNodeActionTarget, SemanticNodeActionTargetError}; pub use semantic_observation::{ MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_CHILDREN, MAX_SEMANTIC_ROLE_BYTES, diff --git a/crates/originweave-core/src/semantic_action_binding.rs b/crates/originweave-core/src/semantic_action_binding.rs new file mode 100644 index 00000000..2786a57c --- /dev/null +++ b/crates/originweave-core/src/semantic_action_binding.rs @@ -0,0 +1,76 @@ +use std::fmt; + +use crate::{ + ActionRequest, BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeHandleError, Origin, + SemanticNodeActionTarget, +}; + +/// One semantic node target explicitly paired with the business action request it would serve. +/// +/// The binding prevents independently validated browser-node authority and business intent from +/// being combined across different source documents. It does not grant policy authority, map a +/// node-local action to business risk, authorize a destination, or execute browser input. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticNodeActionBinding { + target: SemanticNodeActionTarget, + request: ActionRequest, +} + +impl SemanticNodeActionBinding { + /// Bind a semantic node target to a business request from the same current document origin. + pub fn new( + target: SemanticNodeActionTarget, + request: ActionRequest, + ) -> Result { + if target.handle().origin() != request.source_origin() { + return Err(SemanticNodeActionBindingError::SourceOriginMismatch); + } + Ok(Self { target, request }) + } + + /// Return the exact authority-bound semantic node target. + #[must_use] + pub const fn target(&self) -> &SemanticNodeActionTarget { + &self.target + } + + /// Return the independently classified business action request. + #[must_use] + pub const fn request(&self) -> &ActionRequest { + &self.request + } + + /// Revalidate exact browser authority immediately before a later dispatch boundary. + pub fn validate_current( + &self, + current_session: BrowserSessionId, + current_context: BrowsingContextId, + current_origin: &Origin, + current_epoch: DocumentEpoch, + ) -> Result<(), NodeHandleError> { + self.target.validate_current( + current_session, + current_context, + current_origin, + current_epoch, + ) + } +} + +/// A bounded failure to pair browser-node authority with the requested business action. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SemanticNodeActionBindingError { + /// The business request belongs to a different source origin than the observed node. + SourceOriginMismatch, +} + +impl fmt::Display for SemanticNodeActionBindingError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::SourceOriginMismatch => formatter + .write_str("semantic node origin does not match action request source origin"), + } + } +} + +impl std::error::Error for SemanticNodeActionBindingError {}