Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 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
6 changes: 4 additions & 2 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,9 @@ Owns stable value contracts without I/O:
- `InstructionSource` and `SecretDelivery`;
- `RiskClass`, `Capability`, and `ActionKind`;
- exact action, target-origin, and intent-bound `ApprovalScope` and `ApprovalEvidence`;
- `ActionRequest` and `PolicyContext`.
- `ActionRequest` and `PolicyContext`;
- session-, context-, origin-, and epoch-bound `ObservedNodeHandle` values;
- `SameDocumentMutationKind` decisions that rotate `DocumentEpoch` when a same-document mutation can change actionable identity.

### `originweave-policy`

Expand Down Expand Up @@ -167,7 +169,7 @@ Observation should prefer the most structured trustworthy source available:
4. accessibility tree combined with DOM and layout;
5. screenshot or vision fallback for canvas and inaccessible custom interfaces.

Raw HTML is not the default model input. Full snapshots are followed by incremental semantic diffs, versioned by document epoch. Node references become invalid after navigation or epoch change.
Raw HTML is not the default model input. Full snapshots are followed by incremental semantic diffs, versioned by document epoch. Node references become invalid after navigation or after a relevant same-document mutation rotates the epoch.

## 8. Action lifecycle

Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo

### Added

- Same-document mutation kinds that rotate `DocumentEpoch` when a checkout-field replacement, role/name change, accessibility invalidation, frame-document replacement, or actionable subtree replacement can change node identity, while a reviewed non-semantic mutation may keep the current handle and overflow fails closed.
- Refreshed the product and technical gap baseline with the 2026-08-24 live inventory: 158 open pull requests (44 ready, 114 draft), refreshed exact base/head evidence for the #208–#222 release, enterprise-approval, BAP, and WARC/PROV chains, the governance issue additions #212 and #215, and a required-check provider-failure record for the fail-closed Strix re-dispatches on #208/#218/#220.
- Added a dated product and technical gap baseline that separates protected-main implementation truth, active pull-request evidence, live review/check blockers, and the next buyer-visible Phase 1 acceptance work.
- Refreshed the product and technical gap baseline with the current open-PR inventory and exact base/head evidence for the newest Chromium, BAP, extraction, WARC, and idempotency slices.
Expand Down
62 changes: 62 additions & 0 deletions crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -292,6 +292,63 @@ impl DocumentEpoch {
pub const fn value(self) -> u64 {
self.0
}

/// Rotate the epoch when a same-document mutation can change actionable identity.
///
/// Re-observe the current document and use the new handle. Do not reuse a
/// handle emitted at the previous epoch after a relevant mutation.
pub const fn after_same_document_mutation(
self,
mutation: SameDocumentMutationKind,
) -> Result<Self, NodeHandleError> {
if !mutation.requires_epoch_increment() {
return Ok(self);
}
match self.0.checked_add(1) {
Some(next) => Self::new(next),
None => Err(NodeHandleError::DocumentEpochOverflow),
}
}
Comment thread
seonghobae marked this conversation as resolved.
}

/// A same-document lifecycle event that may invalidate actionable node handles.
///
/// This is a control-plane decision input. It does not observe the live DOM,
/// accessibility tree, or browser process.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SameDocumentMutationKind {
/// The previously observed target node was removed from the document.
TargetRemoved,
/// The previously observed target was replaced by a different node.
TargetReplaced,
/// The target's actionable role, accessible name, or actionability changed.
RoleOrNameChanged,
/// A relevant accessibility-tree invalidation changed the target's meaning.
AccessibilityTreeInvalidated,
/// A nested frame document was replaced while the parent document remained.
FrameDocumentReplaced,
/// A subtree mutation replaced the observed actionable target.
ActionableSubtreeReplaced,
/// A reviewed non-semantic change that cannot affect any emitted handle.
NonSemanticUnrelated,
}

impl SameDocumentMutationKind {
/// Return whether this mutation must increment the document epoch.
///
/// Uncertainty must use a relevant variant so the old handle cannot be reused.
#[must_use]
pub const fn requires_epoch_increment(self) -> bool {
match self {
Self::NonSemanticUnrelated => false,
Self::TargetRemoved
| Self::TargetReplaced
| Self::RoleOrNameChanged
| Self::AccessibilityTreeInvalidated
| Self::FrameDocumentReplaced
| Self::ActionableSubtreeReplaced => true,
}
}
}

/// A node identity bound to the exact session, context, origin, and document that produced it.
Expand Down Expand Up @@ -422,6 +479,8 @@ pub enum NodeHandleError {
/// Epoch currently active in the browser context.
current: DocumentEpoch,
},
/// Incrementing the document epoch would wrap the identifier space.
DocumentEpochOverflow,
}

impl fmt::Display for NodeHandleError {
Expand Down Expand Up @@ -456,6 +515,9 @@ impl fmt::Display for NodeHandleError {
observed.value(),
current.value()
),
Self::DocumentEpochOverflow => {
formatter.write_str("document epoch cannot wrap after a same-document mutation")
}
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions crates/originweave-core/tests/document_epoch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,10 @@ fn node_handle_errors_are_standard_and_deterministic_for_adapters() {
},
"observed node document epoch 7 is stale; current epoch is 8".to_owned(),
),
(
NodeHandleError::DocumentEpochOverflow,
"document epoch cannot wrap after a same-document mutation".to_owned(),
),
];

for (error, expected) in cases {
Expand Down
103 changes: 103 additions & 0 deletions crates/originweave-core/tests/same_document_mutation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#![allow(clippy::expect_used)]

use originweave_core::{
BrowserSessionId, BrowsingContextId, DocumentEpoch, NodeHandleError, ObservedNodeHandle,
Origin, SameDocumentMutationKind,
};

fn checkout_field_handle() -> ObservedNodeHandle {
ObservedNodeHandle::new(
BrowserSessionId::new(3).expect("browser session"),
BrowsingContextId::new(11).expect("browsing context"),
Origin::parse("https://shop.example").expect("canonical origin"),
DocumentEpoch::new(1).expect("initial document epoch"),
42,
)
.expect("observed checkout field")
}

#[test]
fn a_same_document_target_replacement_invalidates_the_previous_checkout_field_handle() {
let handle = checkout_field_handle();
let session = handle.browser_session();
let context = handle.browsing_context();
let origin = handle.origin().clone();
let observed_epoch = handle.document_epoch();

let current_epoch = observed_epoch
.after_same_document_mutation(SameDocumentMutationKind::TargetReplaced)
.expect("replacement must rotate the document epoch");

assert_eq!(current_epoch.value(), 2);
assert_eq!(
handle.validate_current(session, context, &origin, current_epoch),
Err(NodeHandleError::StaleDocumentEpoch {
observed: observed_epoch,
current: current_epoch,
})
);

let reobserved = ObservedNodeHandle::new(session, context, origin.clone(), current_epoch, 43)
.expect("re-observed checkout field");
assert_eq!(
reobserved.validate_current(session, context, &origin, current_epoch),
Ok(())
);
}

#[test]
fn relevant_same_document_mutations_rotate_the_document_epoch() {
let epoch = DocumentEpoch::new(4).expect("document epoch");
let cases = [
SameDocumentMutationKind::TargetRemoved,
SameDocumentMutationKind::TargetReplaced,
SameDocumentMutationKind::RoleOrNameChanged,
SameDocumentMutationKind::AccessibilityTreeInvalidated,
SameDocumentMutationKind::FrameDocumentReplaced,
SameDocumentMutationKind::ActionableSubtreeReplaced,
];

for mutation in cases {
assert!(
mutation.requires_epoch_increment(),
"mutation={mutation:?} must rotate the document epoch"
);
let next = epoch
.after_same_document_mutation(mutation)
.expect("relevant mutation must produce the next epoch");
assert_eq!(next.value(), 5, "mutation={mutation:?}");
}
}

#[test]
fn an_unrelated_non_semantic_mutation_may_keep_the_checkout_field_handle() {
let handle = checkout_field_handle();
let session = handle.browser_session();
let context = handle.browsing_context();
let origin = handle.origin().clone();
let observed_epoch = handle.document_epoch();

assert!(!SameDocumentMutationKind::NonSemanticUnrelated.requires_epoch_increment());
let current_epoch = observed_epoch
.after_same_document_mutation(SameDocumentMutationKind::NonSemanticUnrelated)
.expect("unrelated mutation may preserve the epoch");

assert_eq!(current_epoch, observed_epoch);
assert_eq!(
handle.validate_current(session, context, &origin, current_epoch),
Ok(())
);
}

#[test]
fn document_epoch_overflow_fails_closed_instead_of_wrapping() {
let epoch = DocumentEpoch::new(u64::MAX).expect("maximum document epoch");
assert_eq!(
epoch.after_same_document_mutation(SameDocumentMutationKind::TargetRemoved),
Err(NodeHandleError::DocumentEpochOverflow)
);
assert_eq!(
NodeHandleError::DocumentEpochOverflow.to_string(),
"document epoch cannot wrap after a same-document mutation"
);
}
2 changes: 1 addition & 1 deletion docs/adr/0010-session-context-bound-node-authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ OriginWeave represents actionable observed-node authority with five explicit val

`ObservedNodeHandle` stores all five values. Immediately before acting, an adapter must compare the handle with the current session, browsing context, canonical origin, and document epoch. Any mismatch fails closed before the adapter issues a browser action.

The numeric identifiers are internal opaque registry identities. They are not raw WebDriver, WebDriver BiDi, CDP, renderer, process, frame-tree, or DOM identifiers, and they are not durable across process restarts. A protocol adapter must translate external identifiers through a validated, session-scoped registry and allocate collision-free internal identities. It must rotate the document epoch whenever the actionable document lifetime changes, including navigation or document replacement that invalidates earlier node references.
The numeric identifiers are internal opaque registry identities. They are not raw WebDriver, WebDriver BiDi, CDP, renderer, process, frame-tree, or DOM identifiers, and they are not durable across process restarts. A protocol adapter must translate external identifiers through a validated, session-scoped registry and allocate collision-free internal identities. It must rotate the document epoch whenever the actionable document lifetime changes, including navigation, document replacement, or a same-document mutation classified by `SameDocumentMutationKind` as able to change the target's identity or user-visible action semantics.

## Consequences

Expand Down
24 changes: 22 additions & 2 deletions docs/doctoring/session-context-node-authority.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,20 @@ The Rust safety kernel uses independently validated opaque identities for:
- the actionable document lifetime; and
- the adapter-local node identifier.

A future adapter must translate WebDriver, WebDriver BiDi, CDP, renderer, frame-tree, or DOM identifiers through a session-scoped registry. It must allocate a fresh document epoch when navigation or document replacement makes earlier references unsafe. Immediately before an action, it must compare every authority component and fail closed on a session, context, origin, or epoch mismatch.
A future adapter must translate WebDriver, WebDriver BiDi, CDP, renderer, frame-tree, or DOM identifiers through a session-scoped registry. It must allocate a fresh document epoch when navigation, document replacement, or a relevant same-document mutation makes earlier references unsafe. Immediately before an action, it must compare every authority component and fail closed on a session, context, origin, or epoch mismatch.

Same-document mutations are classified by `SameDocumentMutationKind` before any handle is reused. Target removal or replacement, role or accessible-name change, accessibility-tree invalidation, nested-frame document replacement, and actionable subtree replacement increment the epoch. A reviewed non-semantic mutation may keep the current epoch only when the adapter already has deterministic evidence that no emitted handle can change meaning. Overflow of the epoch identifier fails closed. This decision function does not watch the live DOM; the adapter still has to observe MutationObserver, accessibility, or BiDi lifecycle events and feed the matching kind.

```mermaid
flowchart TD
observe[Observe checkout field] --> handle[Emit ObservedNodeHandle at epoch N]
handle --> mutation{Same-document mutation?}
mutation -->|Target replaced, role/name, AX, frame, subtree| rotate[Increment DocumentEpoch]
mutation -->|Reviewed non-semantic only| keep[Keep epoch N]
rotate --> stale[Old handle fails validate_current]
stale --> reobserve[Re-observe and emit epoch N+1 handle]
keep --> reuse[Current handle remains usable]
```

This contract is deliberately stricter than testing only whether a local node number still exists. Same-origin tabs, frames, isolated profiles, or parallel sessions may reuse the same local identifier while referring to different nodes. It also does not convert a valid node handle into an action capability, origin grant, destination grant, approval, or proof of a successful post-condition.

Expand All @@ -31,11 +44,18 @@ This contract is deliberately stricter than testing only whether a local node nu
- same-origin cross-session and cross-context reuse is rejected;
- cross-origin reuse is rejected;
- stale document epochs are rejected;
- a same-document target replacement rotates the epoch and rejects the previous handle;
- a reviewed non-semantic mutation may preserve the current handle;
- document-epoch overflow fails closed;
- errors remain deterministic and implement `std::error::Error`;
- all public contracts remain documented and production behavior remains exactly covered.

## References

World Wide Web Consortium. (2026). *WebDriver*. Retrieved August 9, 2026, from https://w3c.github.io/webdriver/
Web Hypertext Application Technology Working Group. (2026). *DOM standard*. https://dom.spec.whatwg.org/

World Wide Web Consortium. (2023, June 6). *Accessible Rich Internet Applications (WAI-ARIA) 1.2* (W3C Recommendation). https://www.w3.org/TR/wai-aria-1.2/

World Wide Web Consortium. (2026). *WebDriver*. Retrieved August 16, 2026, from https://www.w3.org/TR/webdriver2/
Comment thread
devin-ai-integration[bot] marked this conversation as resolved.

World Wide Web Consortium. (2026). *WebDriver BiDi*. Retrieved August 9, 2026, from https://w3c.github.io/webdriver-bidi/
1 change: 1 addition & 0 deletions docs/product-roadmap.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ Delivered document-node authority foundation:
- a nonzero `BrowsingContextId` for one independently navigable browser context inside that session;
- a nonzero `DocumentEpoch` identity for one observed document lifetime inside that context;
- an `ObservedNodeHandle` bound to the exact browser session, browsing context, canonical origin, document epoch, and nonzero adapter-local node identifier;
- `SameDocumentMutationKind` rotation of `DocumentEpoch` when a same-document replacement, role/name change, accessibility invalidation, frame replacement, or actionable subtree change can alter the target;
- deterministic rejection of cross-session, cross-context, cross-origin, or stale-document node reuse before a future browser adapter performs an action;
- reusable core contracts without Chromium, WebDriver, selector, script-execution, network, storage, or secret dependencies.

Expand Down
2 changes: 1 addition & 1 deletion docs/traceability/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ ADR lifecycle is separate and remains `Proposed`, `Accepted`, `Superseded`, `Dep
| Revocation-material freshness is separate from revocation authenticity/non-revocation | IMPLEMENTED_ON_ACTIVE_PR | ADR 0006/0008 boundary; [`tls-revocation-freshness-authority.md`](tls-revocation-freshness-authority.md) | PR #48 adds a freshness classifier only; protected main still records revocation as NotConfigured and makes no unrevoked claim |
| Proxy/PAC route authority must be explicit | PARTIAL | PRD-NET-005; TRD Section 6.3 | Protected-main direct-route authority exists; PAC evaluation/proxy transport/CONNECT remain incomplete |
| Bounded HTTP semantics require an authenticated governed connection and resource bounds | IMPLEMENTED_ON_ACTIVE_PR | PRD-NET-006; issue #9; active PR #37 | `originweave-http` replacement exists on active PR #37; historical PR #11 is SUPERSEDED implementation lineage and is not current evidence; no protected-main HTTP claim yet |
| Node handles bind session/context/origin/document lifetime | PARTIAL | ADR 0010; PRD-OBS-001/002; TRD Section 5 | Core opaque session/context/document/node authority is on protected main; active PR #40 owns the protocol-ID registry and remains non-shipped evidence |
| Node handles bind session/context/origin/document lifetime | PARTIAL | ADR 0010; PRD-OBS-001/002; TRD Section 5 | Core opaque session/context/document/node authority and `SameDocumentMutationKind` epoch rotation are on this lane; live browser mutation observation remains planned; active PR #40 owns the protocol-ID registry and remains non-shipped evidence |
| Semantic observations retain OriginWeave node authority and explicit source-channel provenance | IMPLEMENTED_ON_ACTIVE_PR | PRD-OBS-001/003/005; ADR 0010; structured-observation architecture | Active PR #52, stacked on #40, implements a bounded `SemanticNodeObservation` value primitive that rejects missing evidence-channel provenance. It is not a browser observation adapter; channels and advertised node actions are descriptive evidence and grant no execution authority |
| Raw secrets never enter model context | PARTIAL | PRD-DATA-001; ADR 0002; TRD Section 9 | Core secret-delivery policy exists; trusted broker/runtime completion remains Planned |
| Sensitive disclosure is purpose- and classification-bound | PARTIAL | ADR 0007; PRD-DATA-002; issue #10 | Purpose-bound policy/evidence foundations are on protected main; active PR #45 adds credential-free handle-lifecycle evidence and #46 adds bounded in-process authoritative use reservation, while trusted storage/revocation/value resolution/cross-process lifecycle/model-disclosure remain open |
Expand Down
Loading