From 64bc330d801dcaddad29cb907fce68a9de367afa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 11:05:32 +0900 Subject: [PATCH 1/5] test(policy): require current semantic observation before dispatch --- ...antic_node_dispatch_current_observation.rs | 157 ++++++++++++++++++ 1 file changed, 157 insertions(+) create mode 100644 crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs diff --git a/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs b/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs new file mode 100644 index 00000000..aac629e0 --- /dev/null +++ b/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs @@ -0,0 +1,157 @@ +use std::cell::Cell; +use std::collections::BTreeSet; + +use originweave_core::{ + ActionIntentDigest, ActionKind, ActionRequest, ApprovalEvidence, BrowserSessionId, + BrowsingContextId, DocumentEpoch, ExecutionPurpose, InstructionSource, NodeActionKind, + ObservationChannel, ObservedNodeHandle, Origin, PolicyContext, RobotsDecision, SecretDelivery, + SemanticNodeActionBinding, SemanticNodeActionTarget, SemanticNodeActionTargetError, + SemanticNodeObservation, SemanticNodeObservationInput, SessionMode, +}; +use originweave_policy::PolicyAuthorizedSemanticNodeAction; + +const VALID_INTENT: &str = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +fn origin(value: &str) -> Result { + Origin::parse(value).map_err(|error| format!("{error:?}")) +} + +fn observation( + node_id: u64, + enabled: bool, + supported_actions: BTreeSet, +) -> Result { + SemanticNodeObservation::new(SemanticNodeObservationInput { + 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())?, + node_id, + ) + .map_err(|error| error.to_string())?, + parent: None, + children: Vec::new(), + role: "button".to_owned(), + accessible_name: "Continue".to_owned(), + visible_text: Some("Continue".to_owned()), + enabled, + visible: true, + selected: None, + supported_actions, + evidence_channels: BTreeSet::from([ObservationChannel::Accessibility]), + }) + .map_err(|error| error.to_string()) +} + +fn authorized_action() -> Result { + let site = origin("https://app.example")?; + let initial_observation = observation(17, true, BTreeSet::from([NodeActionKind::Click]))?; + let target = + SemanticNodeActionTarget::from_observation(&initial_observation, NodeActionKind::Click) + .map_err(|error| error.to_string())?; + let request = ActionRequest::new( + ActionKind::Navigate, + site.clone(), + site.clone(), + InstructionSource::User, + SecretDelivery::None, + ActionIntentDigest::parse(VALID_INTENT).map_err(|error| format!("{error:?}"))?, + ); + let binding = + SemanticNodeActionBinding::new(target, request).map_err(|error| error.to_string())?; + let context = PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + BTreeSet::from([ActionKind::Navigate.required_capability()]), + BTreeSet::from([site.clone()]), + BTreeSet::from([site]), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ); + + PolicyAuthorizedSemanticNodeAction::authorize(binding, &context) + .map_err(|error| error.to_string()) +} + +#[test] +fn exact_current_semantic_observation_reaches_dispatch() -> Result<(), String> { + let authorized = authorized_action()?; + let current = observation(17, true, BTreeSet::from([NodeActionKind::Click]))?; + + let result = authorized + .dispatch_if_current_observation(¤t, |binding| { + (binding.target().action(), binding.request().action()) + }) + .map_err(|error| error.to_string())?; + + assert_eq!(result, (NodeActionKind::Click, ActionKind::Navigate)); + Ok(()) +} + +#[test] +fn newly_disabled_node_never_reaches_dispatch() -> Result<(), String> { + let authorized = authorized_action()?; + let current = observation(17, false, BTreeSet::from([NodeActionKind::Click]))?; + let called = Cell::new(false); + + let error = authorized + .dispatch_if_current_observation(¤t, |_binding| called.set(true)) + .err() + .ok_or_else(|| "disabled current observation unexpectedly dispatched".to_owned())?; + + assert_eq!(error, SemanticNodeActionTargetError::NodeNotEnabled); + assert!(!called.get()); + Ok(()) +} + +#[test] +fn removed_action_never_reaches_dispatch() -> Result<(), String> { + let authorized = authorized_action()?; + let current = observation(17, true, BTreeSet::from([NodeActionKind::ScrollIntoView]))?; + let called = Cell::new(false); + + let error = authorized + .dispatch_if_current_observation(¤t, |_binding| called.set(true)) + .err() + .ok_or_else(|| "removed semantic action unexpectedly dispatched".to_owned())?; + + assert_eq!(error, SemanticNodeActionTargetError::UnsupportedAction); + assert!(!called.get()); + Ok(()) +} + +#[test] +fn different_same_document_node_never_reaches_dispatch() -> Result<(), String> { + let authorized = authorized_action()?; + let current = observation(18, true, BTreeSet::from([NodeActionKind::Click]))?; + let called = Cell::new(false); + + let error = authorized + .dispatch_if_current_observation(¤t, |_binding| called.set(true)) + .err() + .ok_or_else(|| "different semantic node unexpectedly dispatched".to_owned())?; + + assert_eq!( + error, + SemanticNodeActionTargetError::ObservationAuthorityMismatch + ); + assert!(!called.get()); + Ok(()) +} + +#[test] +fn adapter_failure_remains_separate_after_semantic_revalidation() -> Result<(), String> { + let authorized = authorized_action()?; + let current = observation(17, true, BTreeSet::from([NodeActionKind::Click]))?; + + let adapter_result = authorized + .dispatch_if_current_observation(¤t, |_binding| -> Result<(), &'static str> { + Err("adapter failed") + }) + .map_err(|error| error.to_string())?; + + assert_eq!(adapter_result, Err("adapter failed")); + Ok(()) +} From 6b2c11c40fbb7fc6c086a0962119bdaf25f7dc1c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 11:07:29 +0900 Subject: [PATCH 2/5] fix(policy): revalidate current semantic state before dispatch --- .../src/semantic_node_action.rs | 23 ++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/crates/originweave-policy/src/semantic_node_action.rs b/crates/originweave-policy/src/semantic_node_action.rs index 88041146..b2d7d1e3 100644 --- a/crates/originweave-policy/src/semantic_node_action.rs +++ b/crates/originweave-policy/src/semantic_node_action.rs @@ -2,7 +2,7 @@ use std::fmt; use originweave_core::{ BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeHandleError, Origin, PolicyContext, - RiskClass, SemanticNodeActionBinding, + RiskClass, SemanticNodeActionBinding, SemanticNodeActionTargetError, SemanticNodeObservation, }; use crate::{Decision, DenialReason, evaluate}; @@ -81,6 +81,27 @@ impl PolicyAuthorizedSemanticNodeAction { )?; Ok(dispatch(&self.binding)) } + + /// Revalidate one fresh semantic observation and immediately invoke the dispatch callback. + /// + /// The caller must obtain `current_observation` from a trusted browser adapter immediately + /// before the side effect. The callback is not invoked when the observation describes a + /// different OriginWeave-owned node, no longer advertises the selected node-local action, or + /// reports the node disabled for an action that requires enabled state. This method does not + /// obtain or authenticate the observation and does not prove execution success. + pub fn dispatch_if_current_observation( + &self, + current_observation: &SemanticNodeObservation, + dispatch: F, + ) -> Result + where + F: FnOnce(&SemanticNodeActionBinding) -> R, + { + self.binding + .target() + .validate_current_observation(current_observation)?; + Ok(dispatch(&self.binding)) + } } /// A fail-closed outcome that did not produce a policy-authorized semantic-node action. From 2bf613a1d185f6d6dbdc6ede23ebad710d067103 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 11:09:07 +0900 Subject: [PATCH 3/5] docs: record semantic-state dispatch revalidation --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 266fa260..cf5f674b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,6 +26,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Bounded typed semantic node queries over reviewed role, accessible-name, and node-action evidence, without exposing raw DOM/protocol selector languages or granting execution authority. - Authority-bound semantic node action targets that accept only observation-advertised node-local actions and revalidate exact session, context, origin, and document authority before later use without granting policy or browser-execution authority. - Same-call semantic-node dispatch boundary that revalidates exact browser session, browsing context, canonical origin, and document epoch before invoking an already policy-authorized adapter callback, while keeping adapter execution outcome and post-condition proof separate. +- Same-call semantic-state dispatch boundary that requires one fresh exact semantic observation to retain the governed node, selected node-local action, and required enabled state before an already policy-authorized adapter callback can run. - Rust 1.97.1 build contract, strict Clippy and rustdoc gates, and exact production function, line, region, and branch coverage enforcement. - Hourly bounded OpenCode product-development workflow using `NVIDIA_NIM_API_KEY`, an unprivileged disposable workspace, loopback-only model broker, independently verified patches, and publication through a dedicated `OPENCODE_PR_TOKEN` that cannot review or merge. - Architecture, agent, security, contribution, research, database naming, roadmap, quality-gate, and TLS service-identity ADR documentation. From 1c033b60250d2119a4ecece0ce541dd7b6b9d087 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 11:12:53 +0900 Subject: [PATCH 4/5] test(policy): cover semantic dispatch success and denial together --- ...antic_node_dispatch_current_observation.rs | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs b/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs index aac629e0..334dd44e 100644 --- a/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs +++ b/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs @@ -75,18 +75,36 @@ fn authorized_action() -> Result { .map_err(|error| error.to_string()) } +fn dispatch_action( + authorized: &PolicyAuthorizedSemanticNodeAction, + current: &SemanticNodeObservation, + called: &Cell, + adapter_should_fail: bool, +) -> Result, SemanticNodeActionTargetError> { + authorized.dispatch_if_current_observation(current, |binding| { + called.set(true); + if adapter_should_fail { + Err("adapter failed") + } else { + Ok((binding.target().action(), binding.request().action())) + } + }) +} + #[test] fn exact_current_semantic_observation_reaches_dispatch() -> Result<(), String> { let authorized = authorized_action()?; let current = observation(17, true, BTreeSet::from([NodeActionKind::Click]))?; + let called = Cell::new(false); - let result = authorized - .dispatch_if_current_observation(¤t, |binding| { - (binding.target().action(), binding.request().action()) - }) - .map_err(|error| error.to_string())?; + let adapter_result = + dispatch_action(&authorized, ¤t, &called, false).map_err(|error| error.to_string())?; - assert_eq!(result, (NodeActionKind::Click, ActionKind::Navigate)); + assert_eq!( + adapter_result, + Ok((NodeActionKind::Click, ActionKind::Navigate)) + ); + assert!(called.get()); Ok(()) } @@ -96,8 +114,7 @@ fn newly_disabled_node_never_reaches_dispatch() -> Result<(), String> { let current = observation(17, false, BTreeSet::from([NodeActionKind::Click]))?; let called = Cell::new(false); - let error = authorized - .dispatch_if_current_observation(¤t, |_binding| called.set(true)) + let error = dispatch_action(&authorized, ¤t, &called, false) .err() .ok_or_else(|| "disabled current observation unexpectedly dispatched".to_owned())?; @@ -112,8 +129,7 @@ fn removed_action_never_reaches_dispatch() -> Result<(), String> { let current = observation(17, true, BTreeSet::from([NodeActionKind::ScrollIntoView]))?; let called = Cell::new(false); - let error = authorized - .dispatch_if_current_observation(¤t, |_binding| called.set(true)) + let error = dispatch_action(&authorized, ¤t, &called, false) .err() .ok_or_else(|| "removed semantic action unexpectedly dispatched".to_owned())?; @@ -128,8 +144,7 @@ fn different_same_document_node_never_reaches_dispatch() -> Result<(), String> { let current = observation(18, true, BTreeSet::from([NodeActionKind::Click]))?; let called = Cell::new(false); - let error = authorized - .dispatch_if_current_observation(¤t, |_binding| called.set(true)) + let error = dispatch_action(&authorized, ¤t, &called, false) .err() .ok_or_else(|| "different semantic node unexpectedly dispatched".to_owned())?; @@ -145,13 +160,12 @@ fn different_same_document_node_never_reaches_dispatch() -> Result<(), String> { fn adapter_failure_remains_separate_after_semantic_revalidation() -> Result<(), String> { let authorized = authorized_action()?; let current = observation(17, true, BTreeSet::from([NodeActionKind::Click]))?; + let called = Cell::new(false); - let adapter_result = authorized - .dispatch_if_current_observation(¤t, |_binding| -> Result<(), &'static str> { - Err("adapter failed") - }) - .map_err(|error| error.to_string())?; + let adapter_result = + dispatch_action(&authorized, ¤t, &called, true).map_err(|error| error.to_string())?; assert_eq!(adapter_result, Err("adapter failed")); + assert!(called.get()); Ok(()) } From c4c32d4305d6485a5e9f2bf202316b216d95f71f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 11:14:15 +0900 Subject: [PATCH 5/5] style(policy): apply canonical semantic dispatch formatting --- .../tests/semantic_node_dispatch_current_observation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs b/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs index 334dd44e..ed26e1dd 100644 --- a/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs +++ b/crates/originweave-policy/tests/semantic_node_dispatch_current_observation.rs @@ -97,8 +97,8 @@ fn exact_current_semantic_observation_reaches_dispatch() -> Result<(), String> { let current = observation(17, true, BTreeSet::from([NodeActionKind::Click]))?; let called = Cell::new(false); - let adapter_result = - dispatch_action(&authorized, ¤t, &called, false).map_err(|error| error.to_string())?; + let adapter_result = dispatch_action(&authorized, ¤t, &called, false) + .map_err(|error| error.to_string())?; assert_eq!( adapter_result,