Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -33,3 +34,4 @@ pub use semantic_observation::{
SemanticNodeObservationError, SemanticNodeObservationInput, SemanticNodeQuery,
SemanticNodeQueryError,
};
pub use semantic_policy_binding::{SemanticNodePolicyBinding, SemanticNodePolicyBindingError};
59 changes: 59 additions & 0 deletions crates/originweave-core/src/semantic_policy_binding.rs
Original file line number Diff line number Diff line change
@@ -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<Self, SemanticNodePolicyBindingError> {
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 {}
108 changes: 108 additions & 0 deletions crates/originweave-core/tests/semantic_node_policy_binding.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
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<SemanticNodeActionTarget, String> {
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(action: ActionKind, source: &str, target: &str) -> Result<ActionRequest, String> {
Ok(ActionRequest::new(
action,
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(
ActionKind::Submit,
"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(
ActionKind::Submit,
"https://other.example",
"https://example.com",
)?;

assert_eq!(
SemanticNodePolicyBinding::new(target, request).err(),
Some(SemanticNodePolicyBindingError::SourceOriginMismatch)
);
Ok(())
}

#[test]
fn semantic_target_preserves_explicit_cross_origin_policy_target() -> Result<(), String> {
let target = node_target()?;
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!(binding.request(), &request);
assert_eq!(
binding.request().target_origin().as_str(),
"https://other.example"
);
Ok(())
}

#[test]
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"
);
}
Loading