From c8f9cccb37918d70a4fe4c29df1db07519cc7ec3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 12:04:31 +0900 Subject: [PATCH 1/6] test(core): require semantic node policy binding --- .../tests/semantic_node_policy_binding.rs | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 crates/originweave-core/tests/semantic_node_policy_binding.rs diff --git a/crates/originweave-core/tests/semantic_node_policy_binding.rs b/crates/originweave-core/tests/semantic_node_policy_binding.rs new file mode 100644 index 000000000..b7c6326f1 --- /dev/null +++ b/crates/originweave-core/tests/semantic_node_policy_binding.rs @@ -0,0 +1,97 @@ +use std::collections::BTreeSet; + +use originweave_core::{ + ActionIntentDigest, ActionKind, ActionRequest, BrowserSessionId, BrowsingContextId, + DocumentEpoch, InstructionSource, NodeActionKind, ObservationChannel, ObservedNodeHandle, + Origin, SecretDelivery, SemanticNodeActionTarget, SemanticNodeObservation, + SemanticNodeObservationInput, SemanticNodePolicyBinding, SemanticNodePolicyBindingError, +}; + +fn node_target() -> Result { + let handle = ObservedNodeHandle::new( + BrowserSessionId::new(7).map_err(|error| error.to_string())?, + BrowsingContextId::new(11).map_err(|error| error.to_string())?, + Origin::parse("https://example.com").map_err(|error| format!("{error:?}"))?, + 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: "Submit request".to_owned(), + visible_text: Some("Submit request".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())?; + SemanticNodeActionTarget::from_observation(&observation, NodeActionKind::Click) + .map_err(|error| error.to_string()) +} + +fn request(source: &str, target: &str) -> Result { + Ok(ActionRequest::new( + ActionKind::Submit, + Origin::parse(source).map_err(|error| format!("{error:?}"))?, + Origin::parse(target).map_err(|error| format!("{error:?}"))?, + InstructionSource::User, + SecretDelivery::None, + ActionIntentDigest::parse( + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef", + ) + .map_err(|error| format!("{error:?}"))?, + )) +} + +#[test] +fn semantic_target_binds_to_explicit_same_origin_policy_request() -> Result<(), String> { + let target = node_target()?; + let request = request("https://example.com", "https://example.com")?; + let binding = SemanticNodePolicyBinding::new(target.clone(), request.clone()) + .map_err(|error| error.to_string())?; + + assert_eq!(binding.target(), &target); + assert_eq!(binding.request(), &request); + Ok(()) +} + +#[test] +fn semantic_target_rejects_policy_request_from_another_browser_origin() -> Result<(), String> { + let target = node_target()?; + let request = request("https://other.example", "https://example.com")?; + + assert_eq!( + SemanticNodePolicyBinding::new(target, request).err(), + Some(SemanticNodePolicyBindingError::SourceOriginMismatch) + ); + Ok(()) +} + +#[test] +fn semantic_target_rejects_policy_request_for_another_target_origin() -> Result<(), String> { + let target = node_target()?; + let request = request("https://example.com", "https://other.example")?; + + assert_eq!( + SemanticNodePolicyBinding::new(target, request).err(), + Some(SemanticNodePolicyBindingError::TargetOriginMismatch) + ); + Ok(()) +} + +#[test] +fn semantic_policy_binding_errors_are_stable_and_credential_free() { + assert_eq!( + SemanticNodePolicyBindingError::SourceOriginMismatch.to_string(), + "semantic node origin does not match the policy request source origin" + ); + assert_eq!( + SemanticNodePolicyBindingError::TargetOriginMismatch.to_string(), + "semantic node origin does not match the policy request target origin" + ); +} From ca2459f9b5af47d220560b172b0a695dea530b2e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 12:06:38 +0900 Subject: [PATCH 2/6] test(core): preserve explicit policy target semantics --- .../tests/semantic_node_policy_binding.rs | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/crates/originweave-core/tests/semantic_node_policy_binding.rs b/crates/originweave-core/tests/semantic_node_policy_binding.rs index b7c6326f1..6ca81440d 100644 --- a/crates/originweave-core/tests/semantic_node_policy_binding.rs +++ b/crates/originweave-core/tests/semantic_node_policy_binding.rs @@ -34,9 +34,9 @@ fn node_target() -> Result { .map_err(|error| error.to_string()) } -fn request(source: &str, target: &str) -> Result { +fn request(action: ActionKind, source: &str, target: &str) -> Result { Ok(ActionRequest::new( - ActionKind::Submit, + action, Origin::parse(source).map_err(|error| format!("{error:?}"))?, Origin::parse(target).map_err(|error| format!("{error:?}"))?, InstructionSource::User, @@ -51,7 +51,11 @@ fn request(source: &str, target: &str) -> Result { #[test] fn semantic_target_binds_to_explicit_same_origin_policy_request() -> Result<(), String> { let target = node_target()?; - let request = request("https://example.com", "https://example.com")?; + let request = request( + ActionKind::Submit, + "https://example.com", + "https://example.com", + )?; let binding = SemanticNodePolicyBinding::new(target.clone(), request.clone()) .map_err(|error| error.to_string())?; @@ -63,7 +67,11 @@ fn semantic_target_binds_to_explicit_same_origin_policy_request() -> Result<(), #[test] fn semantic_target_rejects_policy_request_from_another_browser_origin() -> Result<(), String> { let target = node_target()?; - let request = request("https://other.example", "https://example.com")?; + let request = request( + ActionKind::Submit, + "https://other.example", + "https://example.com", + )?; assert_eq!( SemanticNodePolicyBinding::new(target, request).err(), @@ -73,25 +81,25 @@ fn semantic_target_rejects_policy_request_from_another_browser_origin() -> Resul } #[test] -fn semantic_target_rejects_policy_request_for_another_target_origin() -> Result<(), String> { +fn semantic_target_preserves_explicit_cross_origin_policy_target() -> Result<(), String> { let target = node_target()?; - let request = request("https://example.com", "https://other.example")?; + let request = request( + ActionKind::Navigate, + "https://example.com", + "https://other.example", + )?; + let binding = SemanticNodePolicyBinding::new(target, request.clone()) + .map_err(|error| error.to_string())?; - assert_eq!( - SemanticNodePolicyBinding::new(target, request).err(), - Some(SemanticNodePolicyBindingError::TargetOriginMismatch) - ); + assert_eq!(binding.request(), &request); + assert_eq!(binding.request().target_origin().as_str(), "https://other.example"); Ok(()) } #[test] -fn semantic_policy_binding_errors_are_stable_and_credential_free() { +fn semantic_policy_binding_error_is_stable_and_credential_free() { assert_eq!( SemanticNodePolicyBindingError::SourceOriginMismatch.to_string(), "semantic node origin does not match the policy request source origin" ); - assert_eq!( - SemanticNodePolicyBindingError::TargetOriginMismatch.to_string(), - "semantic node origin does not match the policy request target origin" - ); } From ab8727a72a65f578f249f8c591c7922a8927c006 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 12:06:50 +0900 Subject: [PATCH 3/6] feat(core): bind semantic targets to explicit policy requests --- .../src/semantic_policy_binding.rs | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 crates/originweave-core/src/semantic_policy_binding.rs diff --git a/crates/originweave-core/src/semantic_policy_binding.rs b/crates/originweave-core/src/semantic_policy_binding.rs new file mode 100644 index 000000000..fb47f7a9b --- /dev/null +++ b/crates/originweave-core/src/semantic_policy_binding.rs @@ -0,0 +1,59 @@ +use std::fmt; + +use crate::{ActionRequest, SemanticNodeActionTarget}; + +/// One semantic browser target paired with the explicit policy request that governs its use. +/// +/// The binding proves only that the policy request starts from the same canonical browser origin +/// that produced the semantic node. It deliberately does not infer an [`crate::ActionKind`], risk +/// class, capability, approval, instruction trust, secret delivery, or target origin from +/// node-local observation evidence. Cross-origin targets therefore remain explicit policy input +/// for the policy engine to evaluate rather than being silently rewritten here. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct SemanticNodePolicyBinding { + target: SemanticNodeActionTarget, + request: ActionRequest, +} + +impl SemanticNodePolicyBinding { + /// Bind one semantic target to an explicit policy request from the same browser origin. + pub fn new( + target: SemanticNodeActionTarget, + request: ActionRequest, + ) -> Result { + if target.handle().origin() != request.source_origin() { + return Err(SemanticNodePolicyBindingError::SourceOriginMismatch); + } + Ok(Self { target, request }) + } + + /// Return the exact authority-bound semantic target supplied by the caller. + #[must_use] + pub const fn target(&self) -> &SemanticNodeActionTarget { + &self.target + } + + /// Return the complete explicit policy request supplied by the caller. + #[must_use] + pub const fn request(&self) -> &ActionRequest { + &self.request + } +} + +/// A fail-closed validation error while pairing semantic target evidence with policy input. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum SemanticNodePolicyBindingError { + /// The policy request claims a browser source origin different from the observed node origin. + SourceOriginMismatch, +} + +impl fmt::Display for SemanticNodePolicyBindingError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::SourceOriginMismatch => formatter + .write_str("semantic node origin does not match the policy request source origin"), + } + } +} + +impl std::error::Error for SemanticNodePolicyBindingError {} From ad6d26699be5bfde8fbfb942005198f4d9596ec7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 12:07:05 +0900 Subject: [PATCH 4/6] feat(core): export semantic policy binding --- crates/originweave-core/src/lib.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index bcd18ddd7..6c5727f2b 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -14,6 +14,7 @@ mod contract_errors; mod contracts; mod semantic_action_target; mod semantic_observation; +mod semantic_policy_binding; pub use browser_registry::{ BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, @@ -33,3 +34,4 @@ pub use semantic_observation::{ SemanticNodeObservationError, SemanticNodeObservationInput, SemanticNodeQuery, SemanticNodeQueryError, }; +pub use semantic_policy_binding::{SemanticNodePolicyBinding, SemanticNodePolicyBindingError}; From 88075398b716ed3edaa10f87ecf8d92233238b64 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 12:07:48 +0900 Subject: [PATCH 5/6] docs: record semantic policy binding boundary --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f1d9106e..ec2c16215 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. +- Explicit semantic-node policy bindings that require the policy request source origin to match the observed browser origin while preserving caller-declared target origin and business-risk semantics for independent policy evaluation. - 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 047921473be4a436a34635542120a2fd319c302a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 12:09:07 +0900 Subject: [PATCH 6/6] style(core): apply canonical semantic binding formatting --- .../originweave-core/tests/semantic_node_policy_binding.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/semantic_node_policy_binding.rs b/crates/originweave-core/tests/semantic_node_policy_binding.rs index 6ca81440d..c745de6ba 100644 --- a/crates/originweave-core/tests/semantic_node_policy_binding.rs +++ b/crates/originweave-core/tests/semantic_node_policy_binding.rs @@ -92,7 +92,10 @@ fn semantic_target_preserves_explicit_cross_origin_policy_target() -> Result<(), .map_err(|error| error.to_string())?; assert_eq!(binding.request(), &request); - assert_eq!(binding.request().target_origin().as_str(), "https://other.example"); + assert_eq!( + binding.request().target_origin().as_str(), + "https://other.example" + ); Ok(()) }