Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 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
f4bc756
merge: align semantic observation with current browser authority
seonghobae Aug 15, 2026
913b21b
merge: align semantic observation with current browser authority
seonghobae Aug 15, 2026
d5efaca
chore(core): align semantic observation stack with current browser au…
seonghobae Aug 17, 2026
52ef3fb
chore(core): align semantic observation stack with current browser au…
seonghobae Aug 17, 2026
0260711
chore(core): align semantic observation with latest browser authority
seonghobae Aug 20, 2026
4380016
chore(core): align semantic observation stack to live browser authority
seonghobae Aug 21, 2026
c84a9c4
fix(core): preserve live browser authority during stack alignment
seonghobae Aug 21, 2026
0553c69
test(core): require registry authority for semantic observations
seonghobae Aug 21, 2026
1f57328
style(core): apply canonical formatting to semantic authority regression
seonghobae Aug 21, 2026
64f503b
fix(core): require live registry authority for semantic observations
seonghobae Aug 21, 2026
575515d
test(core): use live registry authority in semantic observation suite
seonghobae Aug 21, 2026
f71c5c3
style(core): apply canonical semantic observation formatting
seonghobae Aug 21, 2026
957e52c
test(core): cover forged parent observation authority
seonghobae Aug 21, 2026
0e87928
fix(core): remove unreachable relationship coverage branches
seonghobae Aug 21, 2026
c30c043
chore(core): align semantic observation with browser authority root
seonghobae Aug 22, 2026
5cb111b
chore(browser): realign semantic observation stack
seonghobae Aug 23, 2026
6193f18
chore(stack): realign semantic observation on browser authority
seonghobae Aug 23, 2026
adcafe1
docs(changelog): preserve semantic observation slice after stack real…
seonghobae Aug 23, 2026
e9569d3
chore(browser): restack semantic observation on authority registry
seonghobae Aug 24, 2026
ed3297b
docs(browser): restore semantic observation changelog
seonghobae Aug 24, 2026
2455845
merge(browser): reconcile semantic observation with current authority…
seonghobae Aug 27, 2026
8e7a7f2
fix(core): apply canonical formatting after stack reconciliation
seonghobae Aug 27, 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
3 changes: 2 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,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.
- Versioned schema-bound extraction contracts with bounded identifiers and field counts, typed value/cardinality metadata, explicit duplicate-free reviewed source channels, fail-closed schema validation, and deterministic `Display`/`std::error::Error` contracts for public schema failures.
- 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.
Expand Down Expand Up @@ -103,4 +104,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
9 changes: 8 additions & 1 deletion crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
//!
//! This crate keeps the long-lived value contracts in `contracts`, the
//! browser protocol/identifier boundaries and extension authority in focused
//! modules so browser adapters can evolve without turning raw CDP or WebDriver
//! modules, and bounded semantic observations in a separate authority-preserving
//! module so browser adapters can evolve without turning raw CDP or WebDriver
//! metadata into OriginWeave authority.

#![forbid(unsafe_code)]
Expand All @@ -15,6 +16,7 @@ mod browser_registry_coverage;
mod contract_errors;
mod contracts;
mod extension_authority;
mod semantic_observation;

pub use browser_protocol::{
BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolDescriptorError,
Expand All @@ -38,3 +40,8 @@ pub use extension_authority::{
ExtensionAgentGrant as AuthorityExtensionAgentGrant,
evaluate_extension_access as evaluate_extension_authority_access,
};
pub use semantic_observation::{
MAX_ACCESSIBLE_NAME_BYTES, MAX_SEMANTIC_CHILDREN, MAX_SEMANTIC_ROLE_BYTES,
MAX_VISIBLE_TEXT_BYTES, NodeActionKind, ObservationChannel, SemanticNodeObservation,
SemanticNodeObservationError, SemanticNodeObservationInput,
};
11 changes: 7 additions & 4 deletions crates/originweave-core/src/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,13 @@ pub use core_contracts::{
BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolDescriptorError,
BrowserProtocolKind, BrowserRegistryError, BrowserSessionId, BrowsingContextId, Capability,
DocumentEpoch, ExecutionPurpose, ExtensionAgentCapability, ExtensionId, ExtensionIdError,
InstructionSource, MAX_BROWSER_PROTOCOL_METADATA_BYTES, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES,
NodeHandleError, Origin, OriginError, PolicyContext,
RegistryObservedNodeHandle as ObservedNodeHandle, RiskClass, RobotsDecision, SecretDelivery,
SessionMode, evaluate_extension_authority_access as evaluate_extension_access,
InstructionSource, MAX_ACCESSIBLE_NAME_BYTES, MAX_BROWSER_PROTOCOL_METADATA_BYTES,
MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES, MAX_SEMANTIC_CHILDREN, MAX_SEMANTIC_ROLE_BYTES,
MAX_VISIBLE_TEXT_BYTES, NodeActionKind, NodeHandleError, ObservationChannel, Origin,
OriginError, PolicyContext, RegistryObservedNodeHandle as ObservedNodeHandle, RiskClass,
RobotsDecision, SecretDelivery, SemanticNodeObservation, SemanticNodeObservationError,
SemanticNodeObservationInput, SessionMode,
evaluate_extension_authority_access as evaluate_extension_access,
};

/// Stateless MCP routing validation that maps only explicit tools to typed actions.
Expand Down
306 changes: 306 additions & 0 deletions crates/originweave-core/src/semantic_observation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,306 @@
use std::collections::BTreeSet;
use std::fmt;

use crate::{BrowserAuthorityRegistry, 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.
///
/// Every primary or related node handle must still be live authority owned by
/// `registry`. Caller-constructed, retired, stale, or otherwise unbound handles
/// fail closed before page-derived semantic metadata can become an observation.
pub fn new(
input: SemanticNodeObservationInput,
registry: &BrowserAuthorityRegistry,
) -> 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);
}
validate_live_node(registry, &input.handle)?;
if let Some(parent) = input.parent.as_ref() {
validate_live_node(registry, parent)?;
validate_relationship(&input.handle, parent)?;
}
for (index, child) in input.children.iter().enumerate() {
validate_live_node(registry, child)?;
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_live_node(
registry: &BrowserAuthorityRegistry,
handle: &ObservedNodeHandle,
) -> Result<(), SemanticNodeObservationError> {
registry
.validate_node_handle(handle)
.map_err(|_error| SemanticNodeObservationError::UnknownNodeAuthority)
}

fn validate_relationship(
handle: &ObservedNodeHandle,
related: &ObservedNodeHandle,
) -> Result<(), SemanticNodeObservationError> {
if handle == related {
return Err(SemanticNodeObservationError::SelfRelationship);
}
// Both handles have already passed `validate_live_node` against the same registry. A live
// browsing context has exactly one current origin and document epoch, so matching session and
// context authority necessarily implies matching origin and epoch. Rechecking those implied
// dimensions would create unreachable branch states rather than additional defense in depth.
if handle.browser_session() != related.browser_session()
|| handle.browsing_context() != related.browsing_context()
{
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 supplied node handle is not current authority owned by the active browser registry.
UnknownNodeAuthority,
/// 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::UnknownNodeAuthority => formatter.write_str(
"semantic node observation contains node authority not owned by the active browser registry",
),
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