From bad2a3d83a85cb95e7d364221a5bc9bc90489093 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 08:10:57 +0900 Subject: [PATCH 1/5] test(policy): require dispatch-time node revalidation --- .../semantic_node_dispatch_revalidation.rs | 132 ++++++++++++++++++ 1 file changed, 132 insertions(+) create mode 100644 crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs diff --git a/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs b/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs new file mode 100644 index 00000000..0b8ff26f --- /dev/null +++ b/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs @@ -0,0 +1,132 @@ +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, 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 authorized_action() -> Result { + let site = origin("https://app.example")?; + let handle = ObservedNodeHandle::new( + BrowserSessionId::new(7).map_err(|error| error.to_string())?, + BrowsingContextId::new(11).map_err(|error| error.to_string())?, + site.clone(), + DocumentEpoch::new(3).map_err(|error| error.to_string())?, + 17, + ) + .map_err(|error| error.to_string())?; + let observation = 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())?; + let target = SemanticNodeActionTarget::from_observation(&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 dispatch_callback_runs_only_after_exact_browser_revalidation() -> Result<(), String> { + let authorized = authorized_action()?; + let called = Cell::new(false); + + let result = authorized + .dispatch_if_current( + 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())?, + |binding| { + called.set(true); + ( + binding.target().action(), + binding.request().action(), + ) + }, + ) + .map_err(|error| error.to_string())?; + + assert!(called.get()); + assert_eq!(result, (NodeActionKind::Click, ActionKind::Navigate)); + Ok(()) +} + +#[test] +fn stale_browser_authority_never_reaches_dispatch_callback() -> Result<(), String> { + let authorized = authorized_action()?; + let called = Cell::new(false); + + let error = authorized + .dispatch_if_current( + BrowserSessionId::new(7).map_err(|error| error.to_string())?, + BrowsingContextId::new(11).map_err(|error| error.to_string())?, + &origin("https://app.example")?, + DocumentEpoch::new(4).map_err(|error| error.to_string())?, + |_binding| called.set(true), + ) + .err() + .ok_or_else(|| "stale browser authority unexpectedly reached dispatch".to_owned())?; + + assert!(!called.get()); + assert!(error.to_string().contains("stale")); + Ok(()) +} + +#[test] +fn adapter_failure_remains_separate_after_successful_revalidation() -> Result<(), String> { + let authorized = authorized_action()?; + + let adapter_result = authorized + .dispatch_if_current( + 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())?, + |_binding| -> Result<(), &'static str> { Err("adapter failed") }, + ) + .map_err(|error| error.to_string())?; + + assert_eq!(adapter_result, Err("adapter failed")); + Ok(()) +} From fb9aa5833d649148e056652157db890814013705 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 08:14:47 +0900 Subject: [PATCH 2/5] style(policy): apply canonical dispatch revalidation rustfmt --- .../tests/semantic_node_dispatch_revalidation.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs b/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs index 0b8ff26f..323ddbcd 100644 --- a/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs +++ b/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs @@ -51,7 +51,8 @@ fn authorized_action() -> Result { SecretDelivery::None, ActionIntentDigest::parse(VALID_INTENT).map_err(|error| format!("{error:?}"))?, ); - let binding = SemanticNodeActionBinding::new(target, request).map_err(|error| error.to_string())?; + let binding = + SemanticNodeActionBinding::new(target, request).map_err(|error| error.to_string())?; let context = PolicyContext::new( SessionMode::AgentTask, ExecutionPurpose::UserDelegatedTask, @@ -79,10 +80,7 @@ fn dispatch_callback_runs_only_after_exact_browser_revalidation() -> Result<(), DocumentEpoch::new(3).map_err(|error| error.to_string())?, |binding| { called.set(true); - ( - binding.target().action(), - binding.request().action(), - ) + (binding.target().action(), binding.request().action()) }, ) .map_err(|error| error.to_string())?; From 4c3bc7331ed4a8354afe12a6b6e0465937fb3b9a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 08:15:53 +0900 Subject: [PATCH 3/5] feat(policy): revalidate node authority at dispatch boundary --- .../src/semantic_node_action.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/originweave-policy/src/semantic_node_action.rs b/crates/originweave-policy/src/semantic_node_action.rs index 5789587b..88041146 100644 --- a/crates/originweave-policy/src/semantic_node_action.rs +++ b/crates/originweave-policy/src/semantic_node_action.rs @@ -54,6 +54,33 @@ impl PolicyAuthorizedSemanticNodeAction { current_epoch, ) } + + /// Revalidate exact browser authority and immediately invoke one adapter dispatch callback. + /// + /// The supplied session, context, origin, and document epoch must be trusted adapter state + /// sampled for the action that is about to be dispatched. The callback is never invoked when + /// that state no longer matches the semantic-node binding. A successful callback invocation + /// does not authenticate the adapter, grant destination, secret, or approval authority, or + /// prove the action's post-condition; those remain separate execution boundaries. + pub fn dispatch_if_current( + &self, + current_session: BrowserSessionId, + current_context: BrowsingContextId, + current_origin: &Origin, + current_epoch: DocumentEpoch, + dispatch: F, + ) -> Result + where + F: FnOnce(&SemanticNodeActionBinding) -> R, + { + self.validate_current( + current_session, + current_context, + current_origin, + current_epoch, + )?; + Ok(dispatch(&self.binding)) + } } /// A fail-closed outcome that did not produce a policy-authorized semantic-node action. From a897cd19f3f5e2d7c8669af39d8c4c958c4c6d94 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 08:17:34 +0900 Subject: [PATCH 4/5] docs(changelog): record dispatch-time node revalidation --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f1d9106..266fa260 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Authority-bound, bounded semantic node observations with typed node-local action evidence and explicit observation-channel provenance for the first Chromium vertical slice; observation metadata grants no execution authority. - 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. - 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. @@ -76,4 +77,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file From c93b90a316b83a160cf80008cc25c78aa32302f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 12 Aug 2026 09:06:24 +0900 Subject: [PATCH 5/5] test(policy): cover dispatch revalidation generic paths --- .../semantic_node_dispatch_revalidation.rs | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs b/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs index 323ddbcd..c9c4e438 100644 --- a/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs +++ b/crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs @@ -67,6 +67,22 @@ fn authorized_action() -> Result { .map_err(|error| error.to_string()) } +fn dispatch_unit_callback( + authorized: &PolicyAuthorizedSemanticNodeAction, + document_epoch: u64, + called: &Cell, +) -> Result<(), String> { + authorized + .dispatch_if_current( + BrowserSessionId::new(7).map_err(|error| error.to_string())?, + BrowsingContextId::new(11).map_err(|error| error.to_string())?, + &origin("https://app.example")?, + DocumentEpoch::new(document_epoch).map_err(|error| error.to_string())?, + |_binding| called.set(true), + ) + .map_err(|error| error.to_string()) +} + #[test] fn dispatch_callback_runs_only_after_exact_browser_revalidation() -> Result<(), String> { let authorized = authorized_action()?; @@ -95,19 +111,15 @@ fn stale_browser_authority_never_reaches_dispatch_callback() -> Result<(), Strin let authorized = authorized_action()?; let called = Cell::new(false); - let error = authorized - .dispatch_if_current( - BrowserSessionId::new(7).map_err(|error| error.to_string())?, - BrowsingContextId::new(11).map_err(|error| error.to_string())?, - &origin("https://app.example")?, - DocumentEpoch::new(4).map_err(|error| error.to_string())?, - |_binding| called.set(true), - ) + dispatch_unit_callback(&authorized, 3, &called)?; + assert!(called.replace(false)); + + let error = dispatch_unit_callback(&authorized, 4, &called) .err() .ok_or_else(|| "stale browser authority unexpectedly reached dispatch".to_owned())?; assert!(!called.get()); - assert!(error.to_string().contains("stale")); + assert!(error.contains("stale")); Ok(()) }