Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
603ae1c
test(core): require bounded semantic node observation
seonghobae Aug 10, 2026
f876711
style(core): format semantic observation RED contract
seonghobae Aug 10, 2026
3d6fb8c
test(core): isolate semantic observation RED failure
seonghobae Aug 10, 2026
50051da
test(core): specify bounded semantic observation failures
seonghobae Aug 10, 2026
4aae3bc
feat(core): scaffold semantic observation module
seonghobae Aug 10, 2026
59e00a7
feat(core): implement bounded semantic observation
seonghobae Aug 10, 2026
3754f47
feat(core): export semantic observation contract
seonghobae Aug 10, 2026
10a40be
style(core): apply rustfmt to semantic exports
seonghobae Aug 10, 2026
84f609e
style(core): apply rustfmt to semantic observation tests
seonghobae Aug 10, 2026
939ab06
docs(changelog): record semantic observation slice
seonghobae Aug 10, 2026
bda159a
test(core): require semantic observation provenance
seonghobae Aug 10, 2026
df54c61
fix(core): require semantic observation provenance
seonghobae Aug 10, 2026
3f52c1c
test(core): cover provenance validation error
seonghobae Aug 10, 2026
661091d
style(core): apply rustfmt to provenance error
seonghobae Aug 10, 2026
b1bd4f8
test(core): require bounded semantic relationships
seonghobae Aug 10, 2026
e8be794
feat(core): bound semantic node relationships to exact authority
seonghobae Aug 10, 2026
632e724
feat(core): export semantic relationship bound
seonghobae Aug 10, 2026
dbe75ca
style(core): apply canonical semantic relationship formatting
seonghobae Aug 10, 2026
94fd284
test(core): cover each semantic relationship authority axis
seonghobae Aug 10, 2026
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 @@ -22,6 +22,7 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Real loopback rustls integration covering trusted DNS SAN, Common-Name fallback rejection, wrong-name and untrusted-root rejection, fixed-time expiry and not-yet-valid failures, exact IPv4 and IPv6 SANs, TLS 1.2/TLS 1.3, required and optional ALPN, and transport-origin binding.
- Cumulative interactive-first RAM, VRAM, batch, local-model, admission, pause, and compositor-pressure mitigation plans, including active-consumer reduction at exact hard limits.
- Universally value-redacted network evidence with explicit path, metadata, and provenance bounds; ambiguous path rejection; validated source URLs; lowercase SHA-256 identifiers; and verification state.
- 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.
- 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
12 changes: 9 additions & 3 deletions crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Shared security and governance contracts for OriginWeave.
//!
//! This crate keeps the long-lived value contracts in `contracts` and the
//! protocol-identifier registry in a focused module so browser adapters can
//! evolve without turning raw CDP or WebDriver identifiers into authority.
//! This crate keeps the long-lived value contracts in `contracts`, the
//! protocol-identifier registry in a focused module, and bounded semantic
//! observations in a separate authority-preserving module.

#![forbid(unsafe_code)]
#![deny(missing_docs)]
Expand All @@ -11,8 +11,14 @@ mod browser_registry;
#[cfg(test)]
mod browser_registry_coverage;
mod contracts;
mod semantic_observation;

pub use browser_registry::{
BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES,
};
pub use contracts::*;
pub use semantic_observation::{
MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_CHILDREN, MAX_SEMANTIC_ROLE_BYTES,
MAX_VISIBLE_TEXT_BYTES, NodeActionKind, ObservationChannel, SemanticNodeObservation,
SemanticNodeObservationError, SemanticNodeObservationInput,
};
280 changes: 280 additions & 0 deletions crates/originweave-core/src/semantic_observation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,280 @@
use std::collections::BTreeSet;
use std::fmt;

use crate::ObservedNodeHandle;

/// Maximum UTF-8 byte length retained for one semantic node role.
pub const MAX_SEMANTIC_ROLE_BYTES: usize = 64;
/// Maximum UTF-8 byte length retained for one semantic node accessible name.
pub const MAX_ACCESSIBLE_NAME_BYTES: usize = 512;
/// Maximum UTF-8 byte length retained for one semantic node visible-text excerpt.
pub const MAX_VISIBLE_TEXT_BYTES: usize = 4_096;
/// Maximum number of child relationships retained for one semantic node observation.
pub const MAX_SEMANTIC_CHILDREN: usize = 128;

/// A node-local typed action advertised by an observation adapter.
///
/// This is descriptive evidence only and never grants execution authority.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum NodeActionKind {
/// Activate the node using browser-native click semantics.
Click,
/// Insert bounded non-secret text using browser-native input semantics.
TypeText,
/// Select one option using browser-native selection semantics.
SelectOption,
/// Set a checkable control to an explicit checked state.
SetChecked,
/// Scroll the node into the viewport without activating it.
ScrollIntoView,
}

/// A structured evidence channel that contributed to a semantic observation.
///
/// Channel provenance never converts page-provided content into trusted instruction.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ObservationChannel {
/// Experimental structured browser tool metadata, such as WebMCP when available.
WebMcp,
/// Structured data interpreted by a versioned adapter.
StructuredData,
/// Browser accessibility-tree evidence.
Accessibility,
/// Browser DOM evidence used through a bounded adapter.
Dom,
/// Browser layout evidence used through a bounded adapter.
Layout,
/// Bounded visual evidence used when structured channels are insufficient.
Visual,
}

/// Caller-owned fields used to construct one bounded semantic node observation.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticNodeObservationInput {
/// Exact OriginWeave authority handle for the observed node.
pub handle: ObservedNodeHandle,
/// Optional exact-authority parent relationship.
pub parent: Option<ObservedNodeHandle>,
/// Bounded exact-authority child relationships in adapter-observed order.
pub children: Vec<ObservedNodeHandle>,
/// Bounded semantic or accessibility role.
pub role: String,
/// Bounded accessible name; an empty name is valid.
pub accessible_name: String,
/// Optional bounded visible-text excerpt.
pub visible_text: Option<String>,
/// Whether the adapter observed the node as enabled.
pub enabled: bool,
/// Whether the adapter observed the node as visible.
pub visible: bool,
/// Optional selected state when that concept applies.
pub selected: Option<bool>,
/// Finite typed actions the adapter reports as meaningful for this node.
pub supported_actions: BTreeSet<NodeActionKind>,
/// Finite evidence channels that contributed to this observation.
pub evidence_channels: BTreeSet<ObservationChannel>,
}

/// A bounded semantic view of one authority-bound browser node.
///
/// The value carries no raw HTML, protocol-local identifier, or independent authorization.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SemanticNodeObservation {
handle: ObservedNodeHandle,
parent: Option<ObservedNodeHandle>,
children: Vec<ObservedNodeHandle>,
role: String,
accessible_name: String,
visible_text: Option<String>,
enabled: bool,
visible: bool,
selected: Option<bool>,
supported_actions: BTreeSet<NodeActionKind>,
evidence_channels: BTreeSet<ObservationChannel>,
}

impl SemanticNodeObservation {
/// Validate reviewed text, relationship, authority, and provenance bounds.
pub fn new(input: SemanticNodeObservationInput) -> Result<Self, SemanticNodeObservationError> {
if input.role.is_empty() {
return Err(SemanticNodeObservationError::EmptyRole);
}
if input.role.len() > MAX_SEMANTIC_ROLE_BYTES {
return Err(SemanticNodeObservationError::RoleTooLong);
}
if input.accessible_name.len() > MAX_ACCESSIBLE_NAME_BYTES {
return Err(SemanticNodeObservationError::AccessibleNameTooLong);
}
if input
.visible_text
.as_ref()
.is_some_and(|text| text.len() > MAX_VISIBLE_TEXT_BYTES)
{
return Err(SemanticNodeObservationError::VisibleTextTooLong);
}
if input.evidence_channels.is_empty() {
return Err(SemanticNodeObservationError::MissingEvidenceChannel);
}
if input.children.len() > MAX_SEMANTIC_CHILDREN {
return Err(SemanticNodeObservationError::TooManyChildren);
}
if let Some(parent) = input.parent.as_ref() {
validate_relationship(&input.handle, parent)?;
}
for (index, child) in input.children.iter().enumerate() {
validate_relationship(&input.handle, child)?;
if input.children[..index].contains(child) {
return Err(SemanticNodeObservationError::DuplicateChild);
}
}
Ok(Self {
handle: input.handle,
parent: input.parent,
children: input.children,
role: input.role,
accessible_name: input.accessible_name,
visible_text: input.visible_text,
enabled: input.enabled,
visible: input.visible,
selected: input.selected,
supported_actions: input.supported_actions,
evidence_channels: input.evidence_channels,
})
}

/// Return the exact authority-bound node handle.
#[must_use]
pub const fn handle(&self) -> &ObservedNodeHandle {
&self.handle
}

/// Return the optional exact-authority parent relationship.
#[must_use]
pub const fn parent(&self) -> Option<&ObservedNodeHandle> {
self.parent.as_ref()
}

/// Return the bounded exact-authority child relationships in observed order.
#[must_use]
pub fn children(&self) -> &[ObservedNodeHandle] {
&self.children
}

/// Return the bounded semantic role.
#[must_use]
pub fn role(&self) -> &str {
&self.role
}

/// Return the bounded accessible name.
#[must_use]
pub fn accessible_name(&self) -> &str {
&self.accessible_name
}

/// Return the optional bounded visible-text excerpt.
#[must_use]
pub fn visible_text(&self) -> Option<&str> {
self.visible_text.as_deref()
}

/// Return whether the node was observed as enabled.
#[must_use]
pub const fn is_enabled(&self) -> bool {
self.enabled
}

/// Return whether the node was observed as visible.
#[must_use]
pub const fn is_visible(&self) -> bool {
self.visible
}

/// Return the optional selected state.
#[must_use]
pub const fn is_selected(&self) -> Option<bool> {
self.selected
}

/// Return the adapter-advertised node action set.
#[must_use]
pub const fn supported_actions(&self) -> &BTreeSet<NodeActionKind> {
&self.supported_actions
}

/// Return the non-empty evidence-channel provenance set.
#[must_use]
pub const fn evidence_channels(&self) -> &BTreeSet<ObservationChannel> {
&self.evidence_channels
}
}

fn validate_relationship(
handle: &ObservedNodeHandle,
related: &ObservedNodeHandle,
) -> Result<(), SemanticNodeObservationError> {
if handle == related {
return Err(SemanticNodeObservationError::SelfRelationship);
}
if handle.browser_session() != related.browser_session()
|| handle.browsing_context() != related.browsing_context()
|| handle.origin() != related.origin()
|| handle.document_epoch() != related.document_epoch()
{
return Err(SemanticNodeObservationError::RelationshipAuthorityMismatch);
}
Ok(())
}

/// A bounded validation failure for one semantic node observation.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SemanticNodeObservationError {
/// The semantic role was empty.
EmptyRole,
/// The role exceeded [`MAX_SEMANTIC_ROLE_BYTES`].
RoleTooLong,
/// The accessible name exceeded [`MAX_ACCESSIBLE_NAME_BYTES`].
AccessibleNameTooLong,
/// The visible-text excerpt exceeded [`MAX_VISIBLE_TEXT_BYTES`].
VisibleTextTooLong,
/// No evidence channel was supplied for the observation.
MissingEvidenceChannel,
/// The child relationship list exceeded [`MAX_SEMANTIC_CHILDREN`].
TooManyChildren,
/// A relationship crossed the observation's session, context, origin, or document authority.
RelationshipAuthorityMismatch,
/// The observation attempted to relate the node to itself.
SelfRelationship,
/// The child relationship list contained the same exact handle more than once.
DuplicateChild,
}

impl fmt::Display for SemanticNodeObservationError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::EmptyRole => formatter.write_str("semantic node role must not be empty"),
Self::RoleTooLong => formatter.write_str("semantic node role exceeds 64 UTF-8 bytes"),
Self::AccessibleNameTooLong => {
formatter.write_str("semantic node accessible name exceeds 512 UTF-8 bytes")
}
Self::VisibleTextTooLong => {
formatter.write_str("semantic node visible text exceeds 4096 UTF-8 bytes")
}
Self::MissingEvidenceChannel => formatter
.write_str("semantic node observation requires at least one evidence channel"),
Self::TooManyChildren => {
formatter.write_str("semantic node observation exceeds 128 child relationships")
}
Self::RelationshipAuthorityMismatch => formatter.write_str(
"semantic node relationship crosses its session, context, origin, or document authority",
),
Self::SelfRelationship => {
formatter.write_str("semantic node observation cannot relate the node to itself")
}
Self::DuplicateChild => formatter
.write_str("semantic node observation contains a duplicate child relationship"),
}
}
}

impl std::error::Error for SemanticNodeObservationError {}
Loading
Loading