Skip to content
Open
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.
- Credential-safe verified action-outcome evidence that binds action kind, canonical target origin, complete action-intent digest, bounded browser post-condition kind, same-clock dispatch and observation timestamps, and exact provenance; construction rejects unverified or rejected post-conditions and any observation timestamp that predates action dispatch while allowing equal coarse-clock ticks.
- 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
146 changes: 146 additions & 0 deletions crates/originweave-evidence/src/action_outcome.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use std::error::Error;
use std::fmt::{self, Display, Formatter};

use originweave_core::{ActionIntentDigest, ActionKind, Origin};

use crate::{ProvenanceRecord, VerificationResult};

/// A bounded observable state transition that may prove a browser action completed.
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum PostConditionKind {
/// The canonical browser URL changed to the expected resulting location.
UrlChanged,
/// A governed semantic node reached the expected state after the action.
NodeStateChanged,
/// A browser dialog entered the expected visible or closed state.
DialogStateChanged,
/// A bounded network-side mutation attributable to the action was observed.
NetworkMutationObserved,
}

/// A failure to construct successful action evidence from post-condition proof.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum VerifiedActionOutcomeError {
/// The supplied provenance did not independently verify the post-condition.
PostConditionNotVerified,
/// The claimed post-condition observation happened before action dispatch.
PostConditionPredatesDispatch {
/// Monotonic timestamp recorded when the governed action was dispatched.
dispatched_at_milliseconds: u64,
/// Monotonic timestamp recorded when the post-condition was observed.
observed_at_milliseconds: u64,
},
}

impl Display for VerifiedActionOutcomeError {
fn fmt(&self, formatter: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::PostConditionNotVerified => formatter
.write_str("action success requires an independently verified post-condition"),
Self::PostConditionPredatesDispatch {
dispatched_at_milliseconds,
observed_at_milliseconds,
} => write!(
formatter,
"post-condition observation at {observed_at_milliseconds} ms predates action dispatch at {dispatched_at_milliseconds} ms"
),
}
}
}

impl Error for VerifiedActionOutcomeError {}

/// Credential-safe evidence that a typed action completed its verified post-condition.
///
/// Construction is intentionally fail-closed: a command acknowledgement, an
/// unverified observation, rejected provenance, or an observation timestamp
/// earlier than the action dispatch cannot be represented by this type as
/// successful action completion. Equal dispatch and observation timestamps are
/// permitted because a bounded adapter may use a coarse monotonic clock.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct VerifiedActionOutcomeEvidence {
action: ActionKind,
target_origin: Origin,
intent_digest: ActionIntentDigest,
post_condition: PostConditionKind,
dispatched_at_milliseconds: u64,
observed_at_milliseconds: u64,
provenance: ProvenanceRecord,
}

impl VerifiedActionOutcomeEvidence {
/// Create successful action evidence from verified, temporally ordered provenance.
///
/// Both timestamps must come from the same monotonic clock domain. The
/// observation may share the dispatch tick, but it may never predate it.
pub fn new(
action: ActionKind,
target_origin: Origin,
intent_digest: ActionIntentDigest,
post_condition: PostConditionKind,
dispatched_at_milliseconds: u64,
observed_at_milliseconds: u64,
provenance: ProvenanceRecord,
) -> Result<Self, VerifiedActionOutcomeError> {
if provenance.verification_result() != VerificationResult::Verified {
return Err(VerifiedActionOutcomeError::PostConditionNotVerified);
}
if observed_at_milliseconds < dispatched_at_milliseconds {
return Err(VerifiedActionOutcomeError::PostConditionPredatesDispatch {
dispatched_at_milliseconds,
observed_at_milliseconds,
});
}
Ok(Self {
action,
target_origin,
intent_digest,
post_condition,
dispatched_at_milliseconds,
observed_at_milliseconds,
provenance,
})
}

/// Return the typed action whose completion was verified.
#[must_use]
pub const fn action(&self) -> ActionKind {
self.action
}

/// Return the canonical origin affected by the verified action.
#[must_use]
pub const fn target_origin(&self) -> &Origin {
&self.target_origin
}

/// Return the digest of the complete canonical action intent.
#[must_use]
pub const fn intent_digest(&self) -> &ActionIntentDigest {
&self.intent_digest
}

/// Return the bounded post-condition that was independently verified.
#[must_use]
pub const fn post_condition(&self) -> PostConditionKind {
self.post_condition
}

/// Return the monotonic timestamp recorded when action dispatch began.
#[must_use]
pub const fn dispatched_at_milliseconds(&self) -> u64 {
self.dispatched_at_milliseconds
}

/// Return the monotonic timestamp recorded when the post-condition was observed.
#[must_use]
pub const fn observed_at_milliseconds(&self) -> u64 {
self.observed_at_milliseconds
}

/// Return the exact provenance record that verified the post-condition.
#[must_use]
pub const fn provenance(&self) -> &ProvenanceRecord {
&self.provenance
}
}
4 changes: 4 additions & 0 deletions crates/originweave-evidence/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,12 @@
#![forbid(unsafe_code)]
#![deny(missing_docs)]

mod action_outcome;
mod sensitive_access;

pub use action_outcome::{
PostConditionKind, VerifiedActionOutcomeError, VerifiedActionOutcomeEvidence,
};
pub use sensitive_access::{
MAX_SENSITIVE_FIELD_COUNT, MAX_SENSITIVE_IDENTIFIER_BYTES, SensitiveAccessClass,
SensitiveAccessEvidence, SensitiveAccessEvidenceInput, SensitiveAccessOutcome,
Expand Down
157 changes: 157 additions & 0 deletions crates/originweave-evidence/tests/verified_action_outcome.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,157 @@
#![allow(clippy::expect_used)]

use originweave_core::{ActionIntentDigest, ActionKind, Origin};
use originweave_evidence::{
EvidenceSourceKind, PostConditionKind, ProvenanceRecord, VerificationResult,
VerifiedActionOutcomeError, VerifiedActionOutcomeEvidence,
};

const VALID_INTENT: &str =
"sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef";
const VALID_SOURCE_HASH: &str =
"sha256:abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789";
const DISPATCHED_AT_MILLISECONDS: u64 = 1_000;
const OBSERVED_AT_MILLISECONDS: u64 = 1_025;

fn intent() -> ActionIntentDigest {
ActionIntentDigest::parse(VALID_INTENT).expect("valid intent digest")
}

fn origin() -> Origin {
Origin::parse("https://app.example").expect("valid test origin")
}

fn provenance(result: VerificationResult) -> ProvenanceRecord {
ProvenanceRecord::new(
"https://app.example/receipt",
"dom:#receipt-status",
VALID_SOURCE_HASH,
EvidenceSourceKind::DomTree,
result,
)
.expect("valid provenance")
}

#[test]
fn verified_post_condition_can_create_action_success_evidence() {
let target = origin();
let evidence = VerifiedActionOutcomeEvidence::new(
ActionKind::Submit,
target.clone(),
intent(),
PostConditionKind::NodeStateChanged,
DISPATCHED_AT_MILLISECONDS,
OBSERVED_AT_MILLISECONDS,
provenance(VerificationResult::Verified),
)
.expect("verified post-condition should admit success evidence");

assert_eq!(evidence.action(), ActionKind::Submit);
assert_eq!(evidence.target_origin(), &target);
assert_eq!(evidence.intent_digest(), &intent());
assert_eq!(
evidence.post_condition(),
PostConditionKind::NodeStateChanged
);
assert_eq!(
evidence.dispatched_at_milliseconds(),
DISPATCHED_AT_MILLISECONDS
);
assert_eq!(
evidence.observed_at_milliseconds(),
OBSERVED_AT_MILLISECONDS
);
assert_eq!(
evidence.provenance().verification_result(),
VerificationResult::Verified
);
}

#[test]
fn unverified_or_rejected_post_condition_cannot_be_recorded_as_success() {
for result in [VerificationResult::Unverified, VerificationResult::Rejected] {
let error = VerifiedActionOutcomeEvidence::new(
ActionKind::Submit,
origin(),
intent(),
PostConditionKind::NodeStateChanged,
DISPATCHED_AT_MILLISECONDS,
OBSERVED_AT_MILLISECONDS,
provenance(result),
)
.expect_err("non-verified post-condition must fail closed");

assert_eq!(error, VerifiedActionOutcomeError::PostConditionNotVerified);
assert_eq!(
error.to_string(),
"action success requires an independently verified post-condition"
);
assert!(std::error::Error::source(&error).is_none());
}
}

#[test]
fn post_condition_observation_cannot_predate_action_dispatch() {
let error = VerifiedActionOutcomeEvidence::new(
ActionKind::Submit,
origin(),
intent(),
PostConditionKind::NodeStateChanged,
2_000,
1_999,
provenance(VerificationResult::Verified),
)
.expect_err("pre-dispatch observation cannot prove action success");

assert_eq!(
error,
VerifiedActionOutcomeError::PostConditionPredatesDispatch {
dispatched_at_milliseconds: 2_000,
observed_at_milliseconds: 1_999,
}
);
assert_eq!(
error.to_string(),
"post-condition observation at 1999 ms predates action dispatch at 2000 ms"
);
}

#[test]
fn same_monotonic_tick_is_allowed_for_coarse_clock_sources() {
let evidence = VerifiedActionOutcomeEvidence::new(
ActionKind::Submit,
origin(),
intent(),
PostConditionKind::NetworkMutationObserved,
4_000,
4_000,
provenance(VerificationResult::Verified),
)
.expect("coarse monotonic clocks may observe within the dispatch tick");

assert_eq!(evidence.dispatched_at_milliseconds(), 4_000);
assert_eq!(evidence.observed_at_milliseconds(), 4_000);
}

#[test]
fn post_condition_kinds_cover_first_browser_vertical_slice_evidence() {
for kind in [
PostConditionKind::UrlChanged,
PostConditionKind::NodeStateChanged,
PostConditionKind::DialogStateChanged,
PostConditionKind::NetworkMutationObserved,
] {
let evidence = VerifiedActionOutcomeEvidence::new(
ActionKind::Submit,
origin(),
intent(),
kind,
DISPATCHED_AT_MILLISECONDS,
OBSERVED_AT_MILLISECONDS,
provenance(VerificationResult::Verified),
)
.expect("supported post-condition should admit verified evidence");

assert_eq!(evidence.post_condition(), kind);
}
}
Loading