Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules.
- Versioned browser-protocol adapter metadata that distinguishes WebDriver BiDi from pinned CDP, binds bounded adapter/browser revision tokens to an explicit duplicate-free capability set, normalizes capability-set identity independently of caller ordering, and exposes typed fail-closed capability requirements without granting browser, action, network, or secret authority by protocol kind alone.
- Canonical OriginWeave protocol-version parsing for exact `originweave/<major>.<minor>` syntax, with typed fail-closed rejection of malformed, ambiguous, overflowed, or noncanonical serialized generations; parsing does not negotiate compatibility or grant adapter authority.
- Public `require_runtime_revisions` validation that fails closed when caller-supplied runtime protocol or browser revision evidence is malformed or differs from the descriptor's pinned revisions, preserving typed malformed-versus-drift errors without authenticating or attesting the adapter process.
- Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors.
- Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes.
- Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets.
Expand Down
60 changes: 60 additions & 0 deletions crates/originweave-core/src/browser_protocol.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,33 @@ impl BrowserProtocolAdapterDescriptor {
}
}

/// Require exact runtime browser-protocol and browser revisions before use.
///
/// The caller must derive both values from the trusted runtime adapter that
/// is about to perform browser work. This deterministic comparison does not
/// authenticate or attest that caller. It only prevents a descriptor pinned
/// to one validated upstream-protocol/browser pair from being silently used
/// when the supplied runtime evidence is malformed or has drifted.
pub fn require_runtime_revisions(
&self,
protocol_revision: &str,
browser_revision: &str,
) -> Result<(), BrowserProtocolRuntimeRequirementError> {
if !metadata_token_is_valid(protocol_revision) {
return Err(BrowserProtocolRuntimeRequirementError::InvalidProtocolRevision);
}
if !metadata_token_is_valid(browser_revision) {
return Err(BrowserProtocolRuntimeRequirementError::InvalidBrowserRevision);
}
if self.protocol_revision != protocol_revision {
return Err(BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch);
}
if self.browser_revision != browser_revision {
return Err(BrowserProtocolRuntimeRequirementError::BrowserRevisionMismatch);
}
Comment thread
seonghobae marked this conversation as resolved.
Ok(())
}
Comment thread
seonghobae marked this conversation as resolved.

/// Require one explicitly declared adapter capability before later use.
///
/// This method never infers support from the browser protocol family. An
Expand Down Expand Up @@ -317,6 +344,39 @@ impl fmt::Display for BrowserProtocolVersionRequirementError {

impl std::error::Error for BrowserProtocolVersionRequirementError {}

/// Failure to require exact pinned runtime revision evidence from an adapter.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserProtocolRuntimeRequirementError {
/// The runtime upstream-protocol revision token was malformed.
InvalidProtocolRevision,
/// The runtime browser revision token was malformed.
InvalidBrowserRevision,
/// The runtime upstream-protocol revision differs from the pinned descriptor.
ProtocolRevisionMismatch,
/// The runtime browser revision differs from the pinned descriptor.
BrowserRevisionMismatch,
}

impl fmt::Display for BrowserProtocolRuntimeRequirementError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidProtocolRevision => formatter.write_str(
"runtime browser protocol revision must be a bounded ASCII metadata token",
),
Self::InvalidBrowserRevision => formatter
.write_str("runtime browser revision must be a bounded ASCII metadata token"),
Self::ProtocolRevisionMismatch => formatter.write_str(
"runtime browser protocol revision does not match the pinned adapter revision",
),
Self::BrowserRevisionMismatch => formatter.write_str(
"runtime browser revision does not match the pinned adapter browser revision",
),
}
}
}

impl std::error::Error for BrowserProtocolRuntimeRequirementError {}

/// Failure to require one browser protocol capability from an adapter.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum BrowserProtocolCapabilityRequirementError {
Expand Down
5 changes: 3 additions & 2 deletions crates/originweave-core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,9 @@ mod extension_authority;
pub use browser_protocol::{
BrowserProtocolAdapterDescriptor, BrowserProtocolCapability,
BrowserProtocolCapabilityRequirementError, BrowserProtocolDescriptorError, BrowserProtocolKind,
BrowserProtocolVersionRequirementError, MAX_BROWSER_PROTOCOL_METADATA_BYTES,
OriginWeaveProtocolVersion, OriginWeaveProtocolVersionParseError,
BrowserProtocolRuntimeRequirementError, BrowserProtocolVersionRequirementError,
MAX_BROWSER_PROTOCOL_METADATA_BYTES, OriginWeaveProtocolVersion,
OriginWeaveProtocolVersionParseError,
};
pub use browser_registry::{
BrowserAuthorityRegistry, BrowserRegistryError, MAX_EXTERNAL_BROWSER_IDENTIFIER_BYTES,
Expand Down
96 changes: 96 additions & 0 deletions crates/originweave-core/tests/browser_protocol_runtime_revision.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
use std::error::Error;

use originweave_core::{
BrowserProtocolAdapterDescriptor, BrowserProtocolCapability, BrowserProtocolKind,
BrowserProtocolRuntimeRequirementError, OriginWeaveProtocolVersion,
};

const ORIGINWEAVE_PROTOCOL_VERSION: OriginWeaveProtocolVersion =
OriginWeaveProtocolVersion::new(0, 1);
const ADAPTER_VERSION: &str = "originweave-bidi-v1";
const PROTOCOL_REVISION: &str = "webdriver-bidi-wd-2026-06-01";
const BROWSER_REVISION: &str = "chromium-r1639810";

fn descriptor() -> Result<BrowserProtocolAdapterDescriptor, Box<dyn Error>> {
Ok(BrowserProtocolAdapterDescriptor::new(
BrowserProtocolKind::WebDriverBiDi,
ORIGINWEAVE_PROTOCOL_VERSION,
ADAPTER_VERSION,
PROTOCOL_REVISION,
BROWSER_REVISION,
&[BrowserProtocolCapability::Navigation],
)?)
}

#[test]
fn exact_runtime_revisions_are_required_before_adapter_use() -> Result<(), Box<dyn Error>> {
let descriptor = descriptor()?;
assert_eq!(
descriptor.require_runtime_revisions(PROTOCOL_REVISION, BROWSER_REVISION),
Ok(())
);
Ok(())
}

#[test]
fn runtime_revision_drift_fails_closed() -> Result<(), Box<dyn Error>> {
let descriptor = descriptor()?;
assert_eq!(
descriptor.require_runtime_revisions("webdriver-bidi-wd-2026-07-01", BROWSER_REVISION),
Err(BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch)
);
assert_eq!(
descriptor.require_runtime_revisions(PROTOCOL_REVISION, "chromium-r1639811"),
Err(BrowserProtocolRuntimeRequirementError::BrowserRevisionMismatch)
);
assert_eq!(
descriptor.require_runtime_revisions("webdriver-bidi-wd-2026-07-01", "chromium-r1639811"),
Err(BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch)
);
Ok(())
}

#[test]
fn malformed_runtime_revision_evidence_fails_before_comparison() -> Result<(), Box<dyn Error>> {
let descriptor = descriptor()?;
assert_eq!(
descriptor.require_runtime_revisions("webdriver bidi current", BROWSER_REVISION),
Err(BrowserProtocolRuntimeRequirementError::InvalidProtocolRevision)
);
assert_eq!(
descriptor.require_runtime_revisions(PROTOCOL_REVISION, "chromium/current"),
Err(BrowserProtocolRuntimeRequirementError::InvalidBrowserRevision)
);
assert_eq!(
descriptor.require_runtime_revisions("", ""),
Err(BrowserProtocolRuntimeRequirementError::InvalidProtocolRevision)
);
Ok(())
}

#[test]
fn runtime_requirement_errors_are_stable_and_source_free() {
let cases = [
(
BrowserProtocolRuntimeRequirementError::InvalidProtocolRevision,
"runtime browser protocol revision must be a bounded ASCII metadata token",
),
(
BrowserProtocolRuntimeRequirementError::InvalidBrowserRevision,
"runtime browser revision must be a bounded ASCII metadata token",
),
(
BrowserProtocolRuntimeRequirementError::ProtocolRevisionMismatch,
"runtime browser protocol revision does not match the pinned adapter revision",
),
(
BrowserProtocolRuntimeRequirementError::BrowserRevisionMismatch,
"runtime browser revision does not match the pinned adapter browser revision",
),
];

for (error, expected) in cases {
assert_eq!(error.to_string(), expected);
assert!(error.source().is_none());
}
}
8 changes: 7 additions & 1 deletion tests/test_repository_contract.py
Original file line number Diff line number Diff line change
Expand Up @@ -175,6 +175,12 @@ def test_product_name_is_consistent_in_binding_documents(self) -> None:
self.assertNotIn("TraceWeave", text, relative)
self.assertNotIn("ProofRail", text, relative)

def test_runtime_revision_boundary_is_recorded_in_the_changelog(self) -> None:
"""The public runtime-revision boundary must remain visible in release history."""

changelog = (ROOT / "CHANGELOG.md").read_text(encoding="utf-8")
self.assertIn("require_runtime_revisions", changelog)

def test_database_contract_requires_two_word_snake_case(self) -> None:
"""Persistent naming policy must include the mandated canonical form."""

Expand All @@ -185,4 +191,4 @@ def test_database_contract_requires_two_word_snake_case(self) -> None:


if __name__ == "__main__":
unittest.main()
unittest.main()
Loading