Skip to content
Draft
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
3 changes: 2 additions & 1 deletion 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.
- 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.
Expand Down Expand Up @@ -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
27 changes: 27 additions & 0 deletions crates/originweave-policy/src/semantic_node_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<R, F>(
&self,
current_session: BrowserSessionId,
current_context: BrowsingContextId,
current_origin: &Origin,
current_epoch: DocumentEpoch,
dispatch: F,
) -> Result<R, NodeHandleError>
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.
Expand Down
142 changes: 142 additions & 0 deletions crates/originweave-policy/tests/semantic_node_dispatch_revalidation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
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, String> {
Origin::parse(value).map_err(|error| format!("{error:?}"))
}

fn authorized_action() -> Result<PolicyAuthorizedSemanticNodeAction, String> {
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())
}

fn dispatch_unit_callback(
authorized: &PolicyAuthorizedSemanticNodeAction,
document_epoch: u64,
called: &Cell<bool>,
) -> 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()?;
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);

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.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(())
}
Loading