From afea70bee5f479fc0b925be154fe79279fc34ae4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 19:35:10 +0900 Subject: [PATCH 01/25] test(extension): require native messaging host authority --- .../tests/native_messaging_authority.rs | 81 +++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_authority.rs diff --git a/crates/originweave-core/tests/native_messaging_authority.rs b/crates/originweave-core/tests/native_messaging_authority.rs new file mode 100644 index 000000000..74d89be31 --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_authority.rs @@ -0,0 +1,81 @@ +#![allow(clippy::expect_used)] + +use originweave_core::{ + ExtensionId, NativeMessagingAccessDecision, NativeMessagingAccessRequest, + NativeMessagingHostGrant, NativeMessagingHostName, evaluate_native_messaging_access, +}; + +fn extension_id(value: &str) -> ExtensionId { + ExtensionId::parse(value).expect("valid extension id") +} + +fn host_name(value: &str) -> NativeMessagingHostName { + NativeMessagingHostName::parse(value).expect("valid native messaging host name") +} + +#[test] +fn native_messaging_host_name_matches_chromium_manifest_syntax() { + let canonical = "com.contextualwisdom.originweave_host"; + assert_eq!(host_name(canonical).as_str(), canonical); + + for invalid in [ + "", + ".com.contextualwisdom.originweave", + "com.contextualwisdom.originweave.", + "com..contextualwisdom.originweave", + "Com.contextualwisdom.originweave", + "com.contextual-wisdom.originweave", + "com/contextualwisdom/originweave", + "com.contextualwisdom.originweave\n", + "com.contextualwisdom.originweaveฯ€", + ] { + assert!( + NativeMessagingHostName::parse(invalid).is_err(), + "unexpected host name: {invalid:?}" + ); + } +} + +#[test] +fn native_messaging_requires_an_explicit_exact_extension_and_host_grant() { + let allowed_extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let other_extension = extension_id("bcdefghijklmnopabcdefghijklmnopa"); + let allowed_host = host_name("com.contextualwisdom.originweave"); + let other_host = host_name("com.contextualwisdom.other_host"); + let grant = NativeMessagingHostGrant::new(allowed_extension.clone(), allowed_host.clone()); + + let exact = NativeMessagingAccessRequest::new(allowed_extension.clone(), allowed_host.clone()); + assert_eq!( + evaluate_native_messaging_access(&exact, Some(&grant)), + NativeMessagingAccessDecision::Allow + ); + assert_eq!( + evaluate_native_messaging_access(&exact, None), + NativeMessagingAccessDecision::DenyMissingGrant + ); + + let wrong_extension = NativeMessagingAccessRequest::new(other_extension, allowed_host); + assert_eq!( + evaluate_native_messaging_access(&wrong_extension, Some(&grant)), + NativeMessagingAccessDecision::DenyExtensionMismatch + ); + + let wrong_host = NativeMessagingAccessRequest::new(allowed_extension, other_host); + assert_eq!( + evaluate_native_messaging_access(&wrong_host, Some(&grant)), + NativeMessagingAccessDecision::DenyHostMismatch + ); +} + +#[test] +fn native_messaging_grant_does_not_mint_agent_capability() { + let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); + let host = host_name("com.contextualwisdom.originweave"); + let grant = NativeMessagingHostGrant::new(extension.clone(), host.clone()); + let request = NativeMessagingAccessRequest::new(extension, host); + + assert_eq!( + evaluate_native_messaging_access(&request, Some(&grant)), + NativeMessagingAccessDecision::Allow + ); +} From 650244895a7f02f9a47d912ee7ee522bff6ddf06 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 19:40:13 +0900 Subject: [PATCH 02/25] test(extension): prove native host grant cannot mint agent authority --- .../tests/native_messaging_authority.rs | 31 ++++++++++++++++--- 1 file changed, 26 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_authority.rs b/crates/originweave-core/tests/native_messaging_authority.rs index 74d89be31..5bee7744b 100644 --- a/crates/originweave-core/tests/native_messaging_authority.rs +++ b/crates/originweave-core/tests/native_messaging_authority.rs @@ -1,8 +1,10 @@ #![allow(clippy::expect_used)] use originweave_core::{ - ExtensionId, NativeMessagingAccessDecision, NativeMessagingAccessRequest, - NativeMessagingHostGrant, NativeMessagingHostName, evaluate_native_messaging_access, + BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest, + ExtensionAgentCapability, ExtensionId, NativeMessagingAccessDecision, + NativeMessagingAccessRequest, NativeMessagingHostGrant, NativeMessagingHostName, + evaluate_extension_access, evaluate_native_messaging_access, }; fn extension_id(value: &str) -> ExtensionId { @@ -13,6 +15,14 @@ fn host_name(value: &str) -> NativeMessagingHostName { NativeMessagingHostName::parse(value).expect("valid native messaging host name") } +fn session(value: u64) -> BrowserSessionId { + BrowserSessionId::new(value).expect("nonzero browser session") +} + +fn context(value: u64) -> BrowsingContextId { + BrowsingContextId::new(value).expect("nonzero browsing context") +} + #[test] fn native_messaging_host_name_matches_chromium_manifest_syntax() { let canonical = "com.contextualwisdom.originweave_host"; @@ -71,11 +81,22 @@ fn native_messaging_requires_an_explicit_exact_extension_and_host_grant() { fn native_messaging_grant_does_not_mint_agent_capability() { let extension = extension_id("abcdefghijklmnopabcdefghijklmnop"); let host = host_name("com.contextualwisdom.originweave"); - let grant = NativeMessagingHostGrant::new(extension.clone(), host.clone()); - let request = NativeMessagingAccessRequest::new(extension, host); + let native_grant = NativeMessagingHostGrant::new(extension.clone(), host.clone()); + let native_request = NativeMessagingAccessRequest::new(extension.clone(), host); assert_eq!( - evaluate_native_messaging_access(&request, Some(&grant)), + evaluate_native_messaging_access(&native_request, Some(&native_grant)), NativeMessagingAccessDecision::Allow ); + + let agent_request = ExtensionAccessRequest::new( + extension, + session(23), + context(29), + ExtensionAgentCapability::ProposeTypedAction, + ); + assert_eq!( + evaluate_extension_access(&agent_request, None), + ExtensionAccessDecision::DenyMissingGrant + ); } From 639046576c86851158467b9d0cacc05786361adb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 19:42:28 +0900 Subject: [PATCH 03/25] feat(extension): enforce native messaging host allow-list --- crates/originweave-core/src/lib.rs | 118 +++++++++++++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 88dd2e586..a12f43b91 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -1063,3 +1063,121 @@ pub fn evaluate_extension_access( } ExtensionAccessDecision::Allow } + +/// A canonical Chrome native-messaging host name admitted to OriginWeave policy. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub struct NativeMessagingHostName { + canonical: String, +} + +impl NativeMessagingHostName { + /// Parse the host name syntax accepted by Chrome native-messaging manifests. + /// + /// Host names are exact identities rather than display labels: only lowercase + /// ASCII alphanumeric characters, underscores, and dots are accepted. Dots + /// cannot lead, trail, or appear consecutively. + pub fn parse(input: &str) -> Result { + if input.is_empty() + || input.starts_with('.') + || input.ends_with('.') + || input.contains("..") + || !input.bytes().all(|byte| { + byte.is_ascii_lowercase() + || byte.is_ascii_digit() + || byte == b'_' + || byte == b'.' + }) + { + return Err(NativeMessagingHostNameError::InvalidHostName); + } + Ok(Self { + canonical: input.to_owned(), + }) + } + + /// Return the validated native-messaging host name. + #[must_use] + pub fn as_str(&self) -> &str { + &self.canonical + } +} + +/// A validation error for a Chrome native-messaging host name. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeMessagingHostNameError { + /// The value violated Chrome's native-messaging host-name syntax. + InvalidHostName, +} + +/// One explicit host-managed allow-list entry for a Chromium extension. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeMessagingHostGrant { + extension_id: ExtensionId, + host_name: NativeMessagingHostName, +} + +impl NativeMessagingHostGrant { + /// Build one exact extension-to-native-host allow-list entry. + #[must_use] + pub const fn new(extension_id: ExtensionId, host_name: NativeMessagingHostName) -> Self { + Self { + extension_id, + host_name, + } + } +} + +/// One extension request to connect to an exact native-messaging host. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeMessagingAccessRequest { + extension_id: ExtensionId, + host_name: NativeMessagingHostName, +} + +impl NativeMessagingAccessRequest { + /// Build one native-messaging access request without granting process authority. + #[must_use] + pub const fn new(extension_id: ExtensionId, host_name: NativeMessagingHostName) -> Self { + Self { + extension_id, + host_name, + } + } +} + +/// Result of evaluating native-messaging access against one explicit host grant. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeMessagingAccessDecision { + /// The exact extension identity and native host name are explicitly granted. + Allow, + /// No explicit host-managed native-messaging grant was supplied. + DenyMissingGrant, + /// The request belongs to a different extension identity. + DenyExtensionMismatch, + /// The request names a different native-messaging host. + DenyHostMismatch, +} + +/// Evaluate one exact native-messaging request without minting Agent authority. +/// +/// This deterministic primitive models one entry in the native host's explicit +/// extension allow-list. It deliberately does not launch a process, resolve a +/// host path, parse messages, or convert Chrome's `nativeMessaging` permission +/// into an OriginWeave Agent capability. Those remain separate adapter and policy +/// boundaries. +#[must_use] +pub fn evaluate_native_messaging_access( + request: &NativeMessagingAccessRequest, + grant: Option<&NativeMessagingHostGrant>, +) -> NativeMessagingAccessDecision { + let Some(grant) = grant else { + return NativeMessagingAccessDecision::DenyMissingGrant; + }; + if request.extension_id != grant.extension_id { + return NativeMessagingAccessDecision::DenyExtensionMismatch; + } + if request.host_name != grant.host_name { + return NativeMessagingAccessDecision::DenyHostMismatch; + } + NativeMessagingAccessDecision::Allow +} From 9b2bbb7921e3723a0faf5d08719b65671cd7b5f9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 19:49:08 +0900 Subject: [PATCH 04/25] style(extension): apply canonical rustfmt --- crates/originweave-core/src/lib.rs | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index a12f43b91..612601908 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -419,7 +419,7 @@ pub enum NodeHandleError { StaleDocumentEpoch { /// Epoch that originally produced the node handle. observed: DocumentEpoch, - /// Epoch currently active in the browser context. + /// Epoch currently active for the browser context. current: DocumentEpoch, }, } @@ -1082,10 +1082,7 @@ impl NativeMessagingHostName { || input.ends_with('.') || input.contains("..") || !input.bytes().all(|byte| { - byte.is_ascii_lowercase() - || byte.is_ascii_digit() - || byte == b'_' - || byte == b'.' + byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'_' || byte == b'.' }) { return Err(NativeMessagingHostNameError::InvalidHostName); From 28593cf991cc552968da54b722a887252a3695e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 20:00:38 +0900 Subject: [PATCH 05/25] test(extension): cover numeric native host names --- crates/originweave-core/tests/native_messaging_authority.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/native_messaging_authority.rs b/crates/originweave-core/tests/native_messaging_authority.rs index 5bee7744b..f0a830cda 100644 --- a/crates/originweave-core/tests/native_messaging_authority.rs +++ b/crates/originweave-core/tests/native_messaging_authority.rs @@ -25,7 +25,7 @@ fn context(value: u64) -> BrowsingContextId { #[test] fn native_messaging_host_name_matches_chromium_manifest_syntax() { - let canonical = "com.contextualwisdom.originweave_host"; + let canonical = "com.contextualwisdom.originweave_host1"; assert_eq!(host_name(canonical).as_str(), canonical); for invalid in [ From 1c32effe279e4a9b4e1a6a91c5e3713eb98a9795 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 09:36:29 +0900 Subject: [PATCH 06/25] test(extension): bound native messaging host identity --- .../tests/native_messaging_host_bounds.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_host_bounds.rs diff --git a/crates/originweave-core/tests/native_messaging_host_bounds.rs b/crates/originweave-core/tests/native_messaging_host_bounds.rs new file mode 100644 index 000000000..54b69dd1b --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_host_bounds.rs @@ -0,0 +1,20 @@ +#![allow(clippy::expect_used)] + +use originweave_core::{NativeMessagingHostName, NativeMessagingHostNameError}; + +#[test] +fn native_messaging_host_name_is_bounded_before_it_becomes_authority() { + let exact_limit = "a".repeat(256); + assert_eq!( + NativeMessagingHostName::parse(&exact_limit) + .expect("the exact local authority bound remains accepted") + .as_str(), + exact_limit + ); + + let one_over = "a".repeat(257); + assert_eq!( + NativeMessagingHostName::parse(&one_over), + Err(NativeMessagingHostNameError::InvalidHostName) + ); +} From 4ed1ac26af31ceef817976d0cdc1193b85956d8e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 09:42:59 +0900 Subject: [PATCH 07/25] fix(extension): bound native messaging host identity --- crates/originweave-core/src/lib.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 612601908..753034070 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -1064,6 +1064,8 @@ pub fn evaluate_extension_access( ExtensionAccessDecision::Allow } +const MAX_NATIVE_MESSAGING_HOST_NAME_BYTES: usize = 256; + /// A canonical Chrome native-messaging host name admitted to OriginWeave policy. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] pub struct NativeMessagingHostName { @@ -1078,6 +1080,7 @@ impl NativeMessagingHostName { /// cannot lead, trail, or appear consecutively. pub fn parse(input: &str) -> Result { if input.is_empty() + || input.len() > MAX_NATIVE_MESSAGING_HOST_NAME_BYTES || input.starts_with('.') || input.ends_with('.') || input.contains("..") From 8da1772fa6d997ae18b035b9301d5405bc7cb3d2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 22:24:16 +0900 Subject: [PATCH 08/25] test(extension): expose native messaging authority identities --- crates/originweave-core/tests/native_messaging_authority.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/crates/originweave-core/tests/native_messaging_authority.rs b/crates/originweave-core/tests/native_messaging_authority.rs index f0a830cda..5f4f611f1 100644 --- a/crates/originweave-core/tests/native_messaging_authority.rs +++ b/crates/originweave-core/tests/native_messaging_authority.rs @@ -54,7 +54,12 @@ fn native_messaging_requires_an_explicit_exact_extension_and_host_grant() { let other_host = host_name("com.contextualwisdom.other_host"); let grant = NativeMessagingHostGrant::new(allowed_extension.clone(), allowed_host.clone()); + assert_eq!(grant.extension_id(), &allowed_extension); + assert_eq!(grant.host_name(), &allowed_host); + let exact = NativeMessagingAccessRequest::new(allowed_extension.clone(), allowed_host.clone()); + assert_eq!(exact.extension_id(), &allowed_extension); + assert_eq!(exact.host_name(), &allowed_host); assert_eq!( evaluate_native_messaging_access(&exact, Some(&grant)), NativeMessagingAccessDecision::Allow From 427d2f32431139dc7ed59e60df00fd9d0c4eeba0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 01:15:21 +0900 Subject: [PATCH 09/25] feat(extension): expose native messaging authority identities --- crates/originweave-core/src/lib.rs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 753034070..8fd404451 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -1125,6 +1125,18 @@ impl NativeMessagingHostGrant { host_name, } } + + /// Return the extension identity granted native-messaging access. + #[must_use] + pub const fn extension_id(&self) -> &ExtensionId { + &self.extension_id + } + + /// Return the exact native-messaging host identity in this grant. + #[must_use] + pub const fn host_name(&self) -> &NativeMessagingHostName { + &self.host_name + } } /// One extension request to connect to an exact native-messaging host. @@ -1143,6 +1155,18 @@ impl NativeMessagingAccessRequest { host_name, } } + + /// Return the extension identity requesting native-messaging access. + #[must_use] + pub const fn extension_id(&self) -> &ExtensionId { + &self.extension_id + } + + /// Return the exact native-messaging host identity requested. + #[must_use] + pub const fn host_name(&self) -> &NativeMessagingHostName { + &self.host_name + } } /// Result of evaluating native-messaging access against one explicit host grant. From ad1ece96eee7209d27d7fe87001832c412ec71f1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 07:32:19 +0900 Subject: [PATCH 10/25] test(core): align native messaging Agent request scope --- .../tests/native_messaging_authority.rs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/native_messaging_authority.rs b/crates/originweave-core/tests/native_messaging_authority.rs index 5f4f611f1..014930b32 100644 --- a/crates/originweave-core/tests/native_messaging_authority.rs +++ b/crates/originweave-core/tests/native_messaging_authority.rs @@ -3,10 +3,12 @@ use originweave_core::{ BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest, ExtensionAgentCapability, ExtensionId, NativeMessagingAccessDecision, - NativeMessagingAccessRequest, NativeMessagingHostGrant, NativeMessagingHostName, + NativeMessagingAccessRequest, NativeMessagingHostGrant, NativeMessagingHostName, Origin, evaluate_extension_access, evaluate_native_messaging_access, }; +const UNEXPIRED_NOW_EPOCH_SECONDS: u64 = 1_700_000_000; + fn extension_id(value: &str) -> ExtensionId { ExtensionId::parse(value).expect("valid extension id") } @@ -23,6 +25,10 @@ fn context(value: u64) -> BrowsingContextId { BrowsingContextId::new(value).expect("nonzero browsing context") } +fn origin(value: &str) -> Origin { + Origin::parse(value).expect("valid test origin") +} + #[test] fn native_messaging_host_name_matches_chromium_manifest_syntax() { let canonical = "com.contextualwisdom.originweave_host1"; @@ -98,6 +104,8 @@ fn native_messaging_grant_does_not_mint_agent_capability() { extension, session(23), context(29), + origin("https://native-messaging.example"), + UNEXPIRED_NOW_EPOCH_SECONDS, ExtensionAgentCapability::ProposeTypedAction, ); assert_eq!( From 2d16411da59c23cf8e09f944a6c85fc6b749bda0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 20:31:22 +0900 Subject: [PATCH 11/25] test(core): require standard native messaging host errors --- .../tests/native_messaging_host_bounds.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/crates/originweave-core/tests/native_messaging_host_bounds.rs b/crates/originweave-core/tests/native_messaging_host_bounds.rs index 54b69dd1b..ed2fd0f13 100644 --- a/crates/originweave-core/tests/native_messaging_host_bounds.rs +++ b/crates/originweave-core/tests/native_messaging_host_bounds.rs @@ -18,3 +18,13 @@ fn native_messaging_host_name_is_bounded_before_it_becomes_authority() { Err(NativeMessagingHostNameError::InvalidHostName) ); } + +#[test] +fn native_messaging_host_name_error_is_a_standard_credential_safe_error() { + let error = NativeMessagingHostNameError::InvalidHostName; + assert_eq!( + error.to_string(), + "native-messaging host name violates the reviewed Chrome identity syntax" + ); + assert!(std::error::Error::source(&error).is_none()); +} From c639cd78e3acad235be4cbbfdef67b84ce7ddbfa Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 20:36:24 +0900 Subject: [PATCH 12/25] fix(core): expose standard native messaging host errors --- crates/originweave-core/Cargo.toml | 3 +++ crates/originweave-core/src/crate_root.rs | 22 ++++++++++++++++++++++ 2 files changed, 25 insertions(+) create mode 100644 crates/originweave-core/src/crate_root.rs diff --git a/crates/originweave-core/Cargo.toml b/crates/originweave-core/Cargo.toml index 35c83b19b..15bd23167 100644 --- a/crates/originweave-core/Cargo.toml +++ b/crates/originweave-core/Cargo.toml @@ -10,6 +10,9 @@ repository.workspace = true homepage.workspace = true publish = false +[lib] +path = "src/crate_root.rs" + [dependencies] [lints] diff --git a/crates/originweave-core/src/crate_root.rs b/crates/originweave-core/src/crate_root.rs new file mode 100644 index 000000000..9aaca7003 --- /dev/null +++ b/crates/originweave-core/src/crate_root.rs @@ -0,0 +1,22 @@ +//! OriginWeave core contracts with narrow public error integration. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +use std::fmt; + +#[path = "lib.rs"] +mod base; +pub use base::*; + +impl fmt::Display for NativeMessagingHostNameError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidHostName => formatter.write_str( + "native-messaging host name violates the reviewed Chrome identity syntax", + ), + } + } +} + +impl std::error::Error for NativeMessagingHostNameError {} From 48521b2384cce30e95519093e6aad71b98bbd059 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 01:47:37 -0700 Subject: [PATCH 13/25] docs(changelog): record native messaging authority --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 116c02fe6..045685c08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. +- Added bounded native-messaging host-name identity and explicit exact extension-to-host grants so Chrome native-messaging permission or a manifest host name cannot implicitly become OriginWeave Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - 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. From cdf9daa63ddc9ba95a8e6ea0105931918238c848 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 03:41:02 -0700 Subject: [PATCH 14/25] docs: mark merged MCP foundation as protected-main truth --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 045685c08..c3af62d96 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,12 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. -- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. +- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, browsing context, and canonical origin, so a same-session navigation or port change cannot reuse the grant. - Added bounded native-messaging host-name identity and explicit exact extension-to-host grants so Chrome native-messaging permission or a manifest host name cannot implicitly become OriginWeave Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - 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. -- Active PR #168 adds deterministic MCP `2026-07-28` stateless tool-routing foundations with bounded names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. This is active-PR evidence only; the complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned until separately integrated on protected `main`. +- Protected main contains deterministic MCP `2026-07-28` stateless tool-routing foundations with bounded names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. The complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. From 810d3fb5e1066e651e5d7c452f4e757469d6bb9d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 03:42:58 -0700 Subject: [PATCH 15/25] docs: preserve exact origin-grant changelog wording --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c3af62d96..73f03ccab 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. -- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, browsing context, and canonical origin, so a same-session navigation or port change cannot reuse the grant. +- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Added bounded native-messaging host-name identity and explicit exact extension-to-host grants so Chrome native-messaging permission or a manifest host name cannot implicitly become OriginWeave Agent authority. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - 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. From f5776f5f233ac0a7c05e3f4a2846436c23438043 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 08:38:22 -0700 Subject: [PATCH 16/25] feat(extension): bound native messaging framing Merge exact-head native messaging framing boundary into the native-host authority stack. --- CHANGELOG.md | 4 +- .../originweave-core/src/native_messaging.rs | 172 +++++++++++ .../tests/native_messaging_framing.rs | 270 ++++++++++++++++++ docs/doctoring.md | 8 + docs/doctoring/mv3-compatibility.md | 14 +- tests/test_doctoring_reference_contract.py | 25 ++ 6 files changed, 490 insertions(+), 3 deletions(-) create mode 100644 crates/originweave-core/tests/native_messaging_framing.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 7e773752a..3fc5651f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,8 @@ All notable changes to OriginWeave are documented in this file. The format follo - Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline. - Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant. - Added bounded native-messaging host-name identity and explicit exact extension-to-host grants so Chrome native-messaging permission or a manifest host name cannot implicitly become OriginWeave Agent authority. +- Bounded Chrome native-messaging framing with native-endian 32-bit byte lengths, direction-specific 1 MiB host-to-browser and 64 MiB browser-to-host payload ceilings, bounded 64 KiB stream reads that avoid allocating a declared maximum before bytes arrive, fail-closed rejection of oversized, truncated, or trailing frame data, and explicit UTF-8 payload validation before framed bytes can be treated as native-messaging text, without granting Agent authority or claiming JSON trust. +- Pinned the native-messaging compatibility evidence to an immutable Chromium revision and connected its framing, resource-ceiling, and non-authority decisions to the standards doctoring record. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. - 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. @@ -103,4 +105,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 \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD diff --git a/crates/originweave-core/src/native_messaging.rs b/crates/originweave-core/src/native_messaging.rs index 68cbc562a..5c8823842 100644 --- a/crates/originweave-core/src/native_messaging.rs +++ b/crates/originweave-core/src/native_messaging.rs @@ -1,10 +1,14 @@ //! Explicit Chrome native-messaging host authority without ambient Agent authority. use std::fmt; +use std::io::Read; use crate::ExtensionId; const MAX_NATIVE_MESSAGING_HOST_NAME_BYTES: usize = 256; +const HOST_TO_BROWSER_NATIVE_MESSAGING_LIMIT: usize = 1_048_576; +const BROWSER_TO_HOST_NATIVE_MESSAGING_LIMIT: usize = 67_108_864; +const NATIVE_MESSAGING_READ_CHUNK_BYTES: usize = 64 * 1024; /// A canonical Chrome native-messaging host name admitted to OriginWeave policy. #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)] @@ -157,3 +161,171 @@ pub fn evaluate_native_messaging_access( } NativeMessagingAccessDecision::Allow } + +/// Direction of one Chrome native-messaging frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeMessagingFrameDirection { + /// A frame written by a native host for delivery to the browser. + HostToBrowser, + /// A frame written by the browser for delivery to a native host. + BrowserToHost, +} + +/// Failure to encode or decode a bounded Chrome native-messaging frame. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeMessagingFrameError { + /// Fewer than four bytes were available for the native-endian length prefix. + MissingLengthPrefix, + /// The advertised or supplied payload exceeds the limit for its direction. + PayloadTooLarge, + /// The complete frame length differs from the advertised payload length. + LengthMismatch, + /// The framed payload is not valid UTF-8 text. + InvalidUtf8Payload, +} + +impl fmt::Display for NativeMessagingFrameError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::MissingLengthPrefix => { + formatter.write_str("native messaging frame is missing its 32-bit length prefix") + } + Self::PayloadTooLarge => { + formatter.write_str("native messaging payload exceeds the direction-specific limit") + } + Self::LengthMismatch => { + formatter.write_str("native messaging frame length does not match its prefix") + } + Self::InvalidUtf8Payload => { + formatter.write_str("native messaging payload is not valid UTF-8") + } + } + } +} + +impl std::error::Error for NativeMessagingFrameError {} + +/// Failure while reading one bounded native-messaging payload from a stream. +#[derive(Debug)] +pub enum NativeMessagingFrameReadError { + /// Framing policy rejected the advertised payload before payload allocation or I/O. + Frame(NativeMessagingFrameError), + /// The underlying stream failed while reading the prefix or the admitted payload. + Io(std::io::Error), +} + +impl fmt::Display for NativeMessagingFrameReadError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::Frame(error) => error.fmt(formatter), + Self::Io(error) => error.fmt(formatter), + } + } +} + +impl std::error::Error for NativeMessagingFrameReadError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::Frame(error) => Some(error), + Self::Io(error) => Some(error), + } + } +} + +/// Return OriginWeave's reviewed payload ceiling for one native-messaging direction. +#[must_use] +pub const fn native_messaging_payload_limit(direction: NativeMessagingFrameDirection) -> usize { + match direction { + NativeMessagingFrameDirection::HostToBrowser => HOST_TO_BROWSER_NATIVE_MESSAGING_LIMIT, + NativeMessagingFrameDirection::BrowserToHost => BROWSER_TO_HOST_NATIVE_MESSAGING_LIMIT, + } +} + +/// Read one native-messaging payload from a stream after enforcing the direction-specific budget. +/// +/// The four-byte native-endian length prefix is read first. An oversized advertised length is +/// rejected before allocating or reading payload bytes. Stream failures retain their original +/// `std::io::Error` as a causal source. The returned bytes are untrusted framing output only; +/// JSON parsing, provenance, process identity, secrets, and Agent authority remain separate +/// fail-closed boundaries. +pub fn read_native_messaging_payload( + direction: NativeMessagingFrameDirection, + reader: &mut dyn Read, +) -> Result, NativeMessagingFrameReadError> { + let mut prefix = [0_u8; 4]; + reader + .read_exact(&mut prefix) + .map_err(NativeMessagingFrameReadError::Io)?; + + let advertised_length = u32::from_ne_bytes(prefix) as usize; + if advertised_length > native_messaging_payload_limit(direction) { + return Err(NativeMessagingFrameReadError::Frame( + NativeMessagingFrameError::PayloadTooLarge, + )); + } + + let mut payload = Vec::with_capacity(advertised_length.min(NATIVE_MESSAGING_READ_CHUNK_BYTES)); + while payload.len() < advertised_length { + let chunk_start = payload.len(); + let chunk_length = (advertised_length - chunk_start).min(NATIVE_MESSAGING_READ_CHUNK_BYTES); + payload.resize(chunk_start + chunk_length, 0); + reader + .read_exact(&mut payload[chunk_start..]) + .map_err(NativeMessagingFrameReadError::Io)?; + } + Ok(payload) +} + +/// Encode one complete native-messaging frame with a native-endian 32-bit length prefix. +/// +/// The payload is rejected before allocation when it exceeds the direction-specific +/// OriginWeave limit. The returned bytes are framing only and carry no trust or Agent authority. +pub fn encode_native_messaging_frame( + direction: NativeMessagingFrameDirection, + payload: &[u8], +) -> Result, NativeMessagingFrameError> { + if payload.len() > native_messaging_payload_limit(direction) { + return Err(NativeMessagingFrameError::PayloadTooLarge); + } + + let payload_length = payload.len() as u32; + let mut frame = Vec::with_capacity(payload.len() + 4); + frame.extend_from_slice(&payload_length.to_ne_bytes()); + frame.extend_from_slice(payload); + Ok(frame) +} + +/// Decode one complete bounded native-messaging frame without allocating its payload. +/// +/// Oversized advertised lengths are rejected before payload slicing. The frame must +/// contain exactly the advertised payload bytes; truncation and trailing data fail closed. +pub fn decode_native_messaging_frame( + direction: NativeMessagingFrameDirection, + frame: &[u8], +) -> Result<&[u8], NativeMessagingFrameError> { + if frame.len() < 4 { + return Err(NativeMessagingFrameError::MissingLengthPrefix); + } + + let advertised_length = u32::from_ne_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize; + if advertised_length > native_messaging_payload_limit(direction) { + return Err(NativeMessagingFrameError::PayloadTooLarge); + } + if frame.len() != advertised_length + 4 { + return Err(NativeMessagingFrameError::LengthMismatch); + } + + Ok(&frame[4..]) +} + +/// Decode one bounded native-messaging frame and validate its payload as UTF-8 text. +/// +/// This validates only framing and UTF-8 encoding. JSON syntax, message provenance, and +/// any Agent authority remain separate fail-closed boundaries for a later adapter. +pub fn decode_native_messaging_text_frame( + direction: NativeMessagingFrameDirection, + frame: &[u8], +) -> Result<&str, NativeMessagingFrameError> { + let payload = decode_native_messaging_frame(direction, frame)?; + std::str::from_utf8(payload).map_err(|_error| NativeMessagingFrameError::InvalidUtf8Payload) +} diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs new file mode 100644 index 000000000..667ea9587 --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -0,0 +1,270 @@ +use std::error::Error; +use std::io::{Cursor, ErrorKind, Read}; + +use originweave_core::{ + NativeMessagingFrameDirection, NativeMessagingFrameError, NativeMessagingFrameReadError, + decode_native_messaging_frame, decode_native_messaging_text_frame, + encode_native_messaging_frame, native_messaging_payload_limit, read_native_messaging_payload, +}; + +const HOST_TO_BROWSER_LIMIT: usize = 1_048_576; +const BROWSER_TO_HOST_LIMIT: usize = 67_108_864; + +struct RecordingReader { + data: Vec, + offset: usize, + max_request: usize, +} + +impl RecordingReader { + fn new(data: Vec) -> Self { + Self { + data, + offset: 0, + max_request: 0, + } + } +} + +impl Read for RecordingReader { + fn read(&mut self, buffer: &mut [u8]) -> std::io::Result { + self.max_request = self.max_request.max(buffer.len()); + let remaining = &self.data[self.offset..]; + if remaining.is_empty() { + return Ok(0); + } + let amount = remaining.len().min(buffer.len()); + buffer[..amount].copy_from_slice(&remaining[..amount]); + self.offset += amount; + Ok(amount) + } +} + +#[test] +fn native_messaging_payload_limits_are_direction_specific() { + assert_eq!( + native_messaging_payload_limit(NativeMessagingFrameDirection::HostToBrowser), + HOST_TO_BROWSER_LIMIT + ); + assert_eq!( + native_messaging_payload_limit(NativeMessagingFrameDirection::BrowserToHost), + BROWSER_TO_HOST_LIMIT + ); +} + +#[test] +fn native_messaging_frame_round_trip_uses_native_u32_byte_length() -> Result<(), Box> { + let payload = b"{}"; + + for direction in [ + NativeMessagingFrameDirection::HostToBrowser, + NativeMessagingFrameDirection::BrowserToHost, + ] { + let frame = encode_native_messaging_frame(direction, payload)?; + assert_eq!(&frame[..4], &2_u32.to_ne_bytes()); + assert_eq!(decode_native_messaging_frame(direction, &frame)?, payload); + } + Ok(()) +} + +#[test] +fn native_messaging_stream_reader_round_trips_one_bounded_payload() -> Result<(), Box> { + let payload = b"{\"message\":\"bounded\"}"; + let frame = + encode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, payload)?; + let mut reader = Cursor::new(frame); + + assert_eq!( + read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader,)?, + payload + ); + assert_eq!(reader.position(), payload.len() as u64 + 4); + Ok(()) +} + +#[test] +fn native_messaging_stream_reader_rejects_oversized_prefix_before_reading_payload() { + let oversized = (HOST_TO_BROWSER_LIMIT as u32 + 1).to_ne_bytes(); + let mut reader = Cursor::new(oversized); + + assert!(matches!( + read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader,), + Err(NativeMessagingFrameReadError::Frame( + NativeMessagingFrameError::PayloadTooLarge + )) + )); + assert_eq!(reader.position(), 4); +} + +#[test] +fn native_messaging_stream_reader_preserves_truncated_prefix_io_cause() { + let mut reader = Cursor::new([0_u8; 3]); + + assert!(matches!( + read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader), + Err(NativeMessagingFrameReadError::Io(ref error)) + if error.kind() == ErrorKind::UnexpectedEof + )); +} + +#[test] +fn native_messaging_stream_reader_preserves_truncated_payload_io_cause() { + let mut frame = Vec::from(4_u32.to_ne_bytes()); + frame.extend_from_slice(b"abc"); + let mut reader = Cursor::new(frame); + + assert!(matches!( + read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader), + Err(NativeMessagingFrameReadError::Io(ref error)) + if error.kind() == ErrorKind::UnexpectedEof + )); +} + +#[test] +fn native_messaging_stream_reader_bounds_each_payload_read_buffer() { + let prefix = (BROWSER_TO_HOST_LIMIT as u32).to_ne_bytes(); + let mut reader = RecordingReader::new(prefix.to_vec()); + + assert!(matches!( + read_native_messaging_payload(NativeMessagingFrameDirection::BrowserToHost, &mut reader), + Err(NativeMessagingFrameReadError::Io(ref error)) + if error.kind() == ErrorKind::UnexpectedEof + )); + assert!(reader.max_request <= 64 * 1024); +} + +#[test] +fn native_messaging_stream_read_errors_preserve_typed_sources_and_display() { + let frame_error = + NativeMessagingFrameReadError::Frame(NativeMessagingFrameError::PayloadTooLarge); + assert_eq!( + frame_error.to_string(), + "native messaging payload exceeds the direction-specific limit" + ); + assert!(Error::source(&frame_error).is_some()); + + let io_error = + NativeMessagingFrameReadError::Io(std::io::Error::from(ErrorKind::UnexpectedEof)); + assert_eq!(io_error.to_string(), "unexpected end of file"); + let source = Error::source(&io_error); + assert!(source.is_some()); + assert_eq!( + source.and_then(|source| { + source + .downcast_ref::() + .map(std::io::Error::kind) + }), + Some(ErrorKind::UnexpectedEof) + ); +} + +#[test] +fn native_messaging_text_frame_accepts_utf8_and_rejects_invalid_text() -> Result<(), Box> +{ + let utf8_payload = "{\"message\":\"์•ˆ๋…• ๐Ÿ‘‹\"}".as_bytes(); + let utf8_frame = + encode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, utf8_payload)?; + assert_eq!( + decode_native_messaging_text_frame( + NativeMessagingFrameDirection::HostToBrowser, + &utf8_frame, + )?, + "{\"message\":\"์•ˆ๋…• ๐Ÿ‘‹\"}" + ); + + let invalid_utf8_frame = + encode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, &[0xff])?; + assert_eq!( + decode_native_messaging_text_frame( + NativeMessagingFrameDirection::HostToBrowser, + &invalid_utf8_frame, + ), + Err(NativeMessagingFrameError::InvalidUtf8Payload) + ); + assert_eq!( + decode_native_messaging_text_frame( + NativeMessagingFrameDirection::HostToBrowser, + &[0, 0, 0], + ), + Err(NativeMessagingFrameError::MissingLengthPrefix) + ); + Ok(()) +} + +#[test] +fn native_messaging_encoder_rejects_oversized_host_payload_before_framing() { + let oversized = vec![b'x'; HOST_TO_BROWSER_LIMIT + 1]; + + assert_eq!( + encode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, &oversized,), + Err(NativeMessagingFrameError::PayloadTooLarge) + ); +} + +#[test] +fn native_messaging_decoder_rejects_missing_oversized_and_mismatched_lengths() { + assert_eq!( + decode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, &[0, 0, 0],), + Err(NativeMessagingFrameError::MissingLengthPrefix) + ); + + let host_to_browser_oversized = 1_048_577_u32.to_ne_bytes(); + assert_eq!( + decode_native_messaging_frame( + NativeMessagingFrameDirection::HostToBrowser, + &host_to_browser_oversized, + ), + Err(NativeMessagingFrameError::PayloadTooLarge) + ); + + let browser_to_host_oversized = 67_108_865_u32.to_ne_bytes(); + assert_eq!( + decode_native_messaging_frame( + NativeMessagingFrameDirection::BrowserToHost, + &browser_to_host_oversized, + ), + Err(NativeMessagingFrameError::PayloadTooLarge) + ); + + let mut short_frame = Vec::from(4_u32.to_ne_bytes()); + short_frame.extend_from_slice(b"abc"); + assert_eq!( + decode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, &short_frame,), + Err(NativeMessagingFrameError::LengthMismatch) + ); + + let mut trailing_frame = Vec::from(2_u32.to_ne_bytes()); + trailing_frame.extend_from_slice(b"abc"); + assert_eq!( + decode_native_messaging_frame( + NativeMessagingFrameDirection::HostToBrowser, + &trailing_frame, + ), + Err(NativeMessagingFrameError::LengthMismatch) + ); +} + +#[test] +fn native_messaging_frame_errors_are_stable_and_source_free() { + for (error, message) in [ + ( + NativeMessagingFrameError::MissingLengthPrefix, + "native messaging frame is missing its 32-bit length prefix", + ), + ( + NativeMessagingFrameError::PayloadTooLarge, + "native messaging payload exceeds the direction-specific limit", + ), + ( + NativeMessagingFrameError::LengthMismatch, + "native messaging frame length does not match its prefix", + ), + ( + NativeMessagingFrameError::InvalidUtf8Payload, + "native messaging payload is not valid UTF-8", + ), + ] { + assert_eq!(error.to_string(), message); + assert!(Error::source(&error).is_none()); + } +} diff --git a/docs/doctoring.md b/docs/doctoring.md index ec51daaf3..c0efe6155 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -10,6 +10,12 @@ The 1 June 2026 WebDriver BiDi Working Draft defines a bidirectional remote-cont The final Model Context Protocol `2026-07-28` specification defines the currently reviewed MCP generation. Its stateless request model carries protocol metadata per request and standard Streamable HTTP routing metadata for MCP operations; its Tools surface defines bounded, case-sensitive tool names and requires clients to treat tool annotations as untrusted unless supplied by a trusted server. OriginWeave therefore keeps MCP outside the product authority model. Active PR #168 implements only a bounded Rust `tools/call` routing/action-policy foundation for that exact generation; the complete transport, request-metadata, discovery, OAuth, browser, secret, and persistence adapter remains planned and cannot be inferred from the core routing primitive. +### Native messaging framing and authority + +Chrome's native-messaging protocol uses a UTF-8 JSON payload preceded by a 32-bit length in native byte order. Chrome documents a 1 MB native-host-to-browser message limit and a 4 GB browser-to-host protocol envelope; current Chromium source enforces the 1 MiB incoming-host limit before delivery. The nearby 64 MiB value is only a write-size histogram bucket, not a Chrome protocol limit. OriginWeave therefore keeps the browser protocol envelope separate from its own bounded resource policy: the native-messaging framing adapter applies a 64 MiB browser-to-host ceiling, reads admitted stream payloads in bounded 64 KiB chunks rather than allocating the declared maximum up front, rejects incomplete or trailing complete frames, and validates UTF-8 before text handling. It does not parse or trust JSON, launch or authenticate a host, or turn the `nativeMessaging` permission into Agent authority. The executable compatibility and process-ownership boundary remains the security-gated surface documented in `docs/doctoring/mv3-compatibility.md`. + +The reviewed Chromium source is pinned to immutable revision `160af61f9d1316fd1f1dc41e9503cc1f1926d31f` and its file blob `9d205a90d70b0c1c9f0b3b1c5f296528f6b21755`; a mutable branch URL is not reproducible evidence. + ### Browser origin equivalence The WHATWG URL host parser and Chromium canonicalizer classify shortened decimal, integer, hexadecimal, legacy octal-looking, and mixed-component numeric hosts as IPv4 or broken IPv4 candidates rather than ordinary DNS names. Chromium's regression suite includes values such as `192`, `0xC0a80001`, `030052000001`, and mixed hexadecimal components. A non-final empty `0x` component can participate in Chromium's multi-part IPv4 truncation behavior, but a final `0x` label does not produce an IPv4 number because stripping its prefix leaves no digits; it remains a domain label. Chromium also warns that broken IP-like hosts must not be connected because another resolver could accept them. OriginWeave therefore admits only canonical dotted-decimal IPv4 into its policy origin type, rejects browser-special numeric spellings before DNS validation, and preserves final non-numeric DNS labels such as `0x`. @@ -124,6 +130,8 @@ Bonica, R., Cotton, M., Haberman, B., & Vegoda, L. (2017). *Updates to the speci Chromium Authors. (n.d.). *Proxy support in Chrome* [Source documentation]. Chromium. https://chromium.googlesource.com/chromium/src/+/a3e71ebfa307d8760eb68b777e2998a869940092/net/docs/proxy.md +Chromium Authors. (2026). *native_message_process_host.cc* [Source code, blob `9d205a90d70b0c1c9f0b3b1c5f296528f6b21755` at revision `160af61f9d1316fd1f1dc41e9503cc1f1926d31f`]. Chromium. https://chromium.googlesource.com/chromium/src/+/160af61f9d1316fd1f1dc41e9503cc1f1926d31f/chrome/browser/extensions/api/messaging/native_message_process_host.cc + Chromium Authors. (2026). *URL canonicalizer unit tests* [Source code]. Chromium. https://chromium.googlesource.com/chromium/src/+/446d05d21720f0b3505ec21057b3e9f909784262/url/url_canon_unittest.cc Cooper, D., Santesson, S., Farrell, S., Boeyen, S., Housley, R., & Polk, W. (2008). *Internet X.509 public key infrastructure certificate and certificate revocation list (CRL) profile* (RFC 5280). Internet Engineering Task Force. https://doi.org/10.17487/RFC5280 diff --git a/docs/doctoring/mv3-compatibility.md b/docs/doctoring/mv3-compatibility.md index 571c49329..4f658463c 100644 --- a/docs/doctoring/mv3-compatibility.md +++ b/docs/doctoring/mv3-compatibility.md @@ -1,7 +1,7 @@ # Manifest V3 compatibility evidence baseline - **Status:** Active implementation evidence for issue #27 -- **Reviewed:** 2026-08-11 +- **Reviewed:** 2026-08-28 - **Pinned browser:** Chrome for Testing `150.0.7871.129`, Chromium revision `r1639810` OriginWeave uses Chromium as its compatibility kernel, so browser-extension compatibility must be demonstrated with executable Chromium evidence rather than inferred from architecture alone. The protected-main lane exercises a controlled unpacked Manifest V3 extension against one exact Chrome for Testing build and proves service-worker, content-script, storage, declarative-network-request, tabs, windows, scripting, commands, side-panel, bookmarks/history read compatibility, restart persistence, repeatability, and one real WebDriver click/post-condition. Active stacked compatibility work adds downloads, bounded bookmark/history mutation, profile isolation, explicit extension update/version-migration evidence, and an exact content-script isolated-world check. OriginWeave does **not claim 100% Chrome extension compatibility**. @@ -29,7 +29,7 @@ This matrix separates protected-main executable evidence from active, non-shippe | Per-trial Agent Task profile isolation | **ACTIVE_PR #49** | Compatibility trials use isolated ephemeral profiles rather than ambient human state. | Full production Agent Task browser orchestration remains issue #28 work. | | Extension update/version migration | **ACTIVE_PR #60** | Trial-local extension copy transitions `1.0.0` โ†’ `1.0.1` on the same ephemeral profile; versioned storage state is required to migrate and real pinned-Chromium evidence reports the update-migration surface. | No Chrome Web Store updater, enterprise deployment channel, arbitrary downgrade, or protected-main release claim. | | Managed enterprise extension policy | **PLANNED** | No protected-main executable compatibility proof yet. | Do not infer managed-policy support from Chromium ancestry alone. | -| Native messaging | **PLANNED / SECURITY-GATED** | No compatibility claim. | Future support requires an explicit host-managed allow-list and process boundary. | +| Native messaging | **PLANNED / SECURITY-GATED** | Active PR #82 defines exact extension-to-host authority and stacked Draft #154 defines bounded binary framing plus UTF-8 payload validation, but neither is real pinned-Chromium native-host compatibility evidence. | Process launch/registration ownership, JSON syntax/semantic parsing, untrusted-message classification, sandboxing, real stdio integration, and executable browser compatibility remain unproven. | | Google-only services, proprietary codecs, DRM, Web Store licensing | **OUT_OF_SCOPE FOR COMPATIBILITY CLAIM** | Deliberately excluded from the open compatibility claim. | Chromium/API compatibility must not be conflated with Google service or licensing equivalence. | The release-quality capability matrix must remain coupled to executable evidence. Adding a row to documentation never creates support; declaring a new supported capability must first add a realistic regression test and pinned-Chromium proof. Conversely, if a declared protected-main capability regresses, the release gate must fail rather than silently downgrading the matrix. @@ -46,6 +46,12 @@ Restart persistence and extension update migration are separate compatibility cl Content-script injection and content-script JavaScript isolation are separate compatibility claims. Active PR #61 writes `window.originweaveWorldSentinel = "page"` in the fixture page's main world and repeatedly publishes that value through one controlled DOM attribute. The content script assigns the same global name to `"extension"` in its own execution world, waits a bounded interval, and only reports the existing compatibility surface ready when it simultaneously observes the page's published `page` value and its own `extension` value. If both scripts share one JavaScript global namespace, the page publisher changes to `extension` and real-browser compatibility fails. DOM sharing here is deliberate test evidence, not permission for arbitrary page content to become trusted instruction or Agent authority. +## Native-messaging protocol boundary + +Chrome's native-messaging protocol uses a UTF-8 JSON message preceded by a 32-bit payload length in native byte order. Chrome's documented protocol ceiling is 1 MB for a message sent by the native host to the browser and 4 GB for a message sent by the browser to the native host. Current Chromium source independently enforces the 1 MiB incoming-host ceiling before delivering host data. Its extension-to-host write path encodes the payload length through a checked `uint32_t`; the nearby 64 MiB value is the upper bucket used by the `Extensions.NativeMessaging.MessageSize.Extension` histogram, not an enforced Chrome protocol ceiling. The reviewed source content is identified by Chromium blob `9d205a90d70b0c1c9f0b3b1c5f296528f6b21755`. + +Draft PR #154 therefore mirrors Chrome's 1 MiB host-to-browser safety boundary but deliberately applies a stricter **OriginWeave-owned 64 MiB resource ceiling** to browser-to-host frames. That local bound limits allocation and buffering below Chrome's protocol envelope; it must not be described as a Chrome compatibility maximum. The reusable Rust boundary rejects oversized encoder input before allocation, rejects an oversized advertised decoder length before payload slicing, reads admitted stream payloads in 64 KiB chunks instead of committing the full declared ceiling before bytes arrive, requires the complete frame length to equal the advertised byte count so truncation and trailing data fail closed, and rejects invalid UTF-8 before a caller can treat framed bytes as native-messaging text. It still does not validate JSON syntax or semantics, trust the decoded text, launch or authenticate a native-host process, validate operating-system registration, or convert Chrome `nativeMessaging` permission into OriginWeave Agent authority. + ## Supply-chain and repeatability evidence The CI lane downloads the exact Chrome/ChromeDriver version from the official Chrome for Testing public bucket, records SHA-256 receipts for the downloaded archives, verifies the runtime-reported browser version, and emits bounded JSON compatibility evidence. A future release-quality matrix should additionally pin published artifact digests or equivalent immutable supply-chain identity when the upstream distribution exposes that identity in an authoritative machine-readable form. @@ -64,6 +70,10 @@ Chrome for Developers. (n.d.). *chrome.history*. Google. Retrieved August 11, 20 Chrome for Developers. (n.d.). *Manifest file format*. Google. Retrieved August 9, 2026, from https://developer.chrome.com/docs/extensions/reference/manifest +Chrome for Developers. (n.d.). *Native messaging*. Google. Retrieved August 24, 2026, from https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging + +Chromium Authors. (2026). *native_message_process_host.cc* [Source code, blob `9d205a90d70b0c1c9f0b3b1c5f296528f6b21755` at revision `160af61f9d1316fd1f1dc41e9503cc1f1926d31f`]. Chromium. https://chromium.googlesource.com/chromium/src/+/160af61f9d1316fd1f1dc41e9503cc1f1926d31f/chrome/browser/extensions/api/messaging/native_message_process_host.cc + Bynens, M. (2023, June 12). *Chrome for Testing*. Chrome for Developers. https://developer.chrome.com/docs/automation-and-testing/chrome-for-testing Google Chrome Labs. (2026, July 21). *Chrome for Testing availability*. https://googlechromelabs.github.io/chrome-for-testing/ diff --git a/tests/test_doctoring_reference_contract.py b/tests/test_doctoring_reference_contract.py index bdeded44f..a805778c4 100644 --- a/tests/test_doctoring_reference_contract.py +++ b/tests/test_doctoring_reference_contract.py @@ -23,6 +23,31 @@ def test_rfc_5280_reference_uses_canonical_author_initials(self) -> None: ) self.assertIn(expected, text) + def test_native_messaging_decision_trace_is_pinned(self) -> None: + """Native messaging framing evidence must remain traceable and immutable.""" + text = DOCTORING.read_text(encoding="utf-8") + compatibility = ( + ROOT / "docs" / "doctoring" / "mv3-compatibility.md" + ).read_text(encoding="utf-8") + for expected in ( + "### Native messaging framing and authority", + "Chrome's native-messaging protocol uses a UTF-8 JSON payload", + "64 MiB browser-to-host ceiling", + "Chromium Authors. (2026). *native_message_process_host.cc*", + "160af61f9d1316fd1f1dc41e9503cc1f1926d31f", + ): + with self.subTest(expected=expected): + self.assertIn(expected, text) + self.assertIn( + "https://chromium.googlesource.com/chromium/src/+/160af61f9d1316fd1f1dc41e9503cc1f1926d31f/", + compatibility, + ) + self.assertNotIn( + "https://chromium.googlesource.com/chromium/src/+/refs/heads/main/" + "chrome/browser/extensions/api/messaging/native_message_process_host.cc", + compatibility, + ) + if __name__ == "__main__": unittest.main() From 89fe04beafaa6f7f0ad8b15ed9a3db2b5e572fb2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:11:29 -0700 Subject: [PATCH 17/25] test(native-messaging): require exact manifest access policy --- .../tests/native_messaging_manifest_policy.rs | 138 ++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_manifest_policy.rs diff --git a/crates/originweave-core/tests/native_messaging_manifest_policy.rs b/crates/originweave-core/tests/native_messaging_manifest_policy.rs new file mode 100644 index 000000000..d14214963 --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_manifest_policy.rs @@ -0,0 +1,138 @@ +#![allow(clippy::expect_used)] + +use std::error::Error; + +use originweave_core::{ + ExtensionId, NativeMessagingAllowedOrigin, NativeMessagingAllowedOriginError, + NativeMessagingHostName, NativeMessagingInterfaceType, NativeMessagingInterfaceTypeError, + NativeMessagingManifestAccessPolicy, NativeMessagingManifestAccessPolicyError, +}; + +const ALLOWED_EXTENSION_ID: &str = "abcdefghijklmnopabcdefghijklmnop"; +const OTHER_EXTENSION_ID: &str = "bcdefghijklmnopabcdefghijklmnopa"; + +fn extension_id(value: &str) -> ExtensionId { + ExtensionId::parse(value).expect("valid extension id") +} + +fn allowed_origin(value: &str) -> NativeMessagingAllowedOrigin { + NativeMessagingAllowedOrigin::parse(value).expect("valid allowed origin") +} + +fn host_name() -> NativeMessagingHostName { + NativeMessagingHostName::parse("com.contextualwisdom.originweave") + .expect("valid native messaging host name") +} + +#[test] +fn native_messaging_allowed_origin_requires_one_exact_chrome_extension_origin() { + let raw = format!("chrome-extension://{ALLOWED_EXTENSION_ID}/"); + let origin = allowed_origin(&raw); + + assert_eq!(origin.extension_id(), &extension_id(ALLOWED_EXTENSION_ID)); + assert_eq!(origin.as_str(), raw); + + for invalid in [ + "chrome-extension://*/", + "chrome-extension://abcdefghijklmnopabcdefghijklmnop", + "chrome-extension://abcdefghijklmnopabcdefghijklmnop/path", + "https://abcdefghijklmnopabcdefghijklmnop/", + "chrome-extension://ABCDEFGHIJKLMNOPABCDEFGHIJKLMNOP/", + ] { + assert_eq!( + NativeMessagingAllowedOrigin::parse(invalid), + Err(NativeMessagingAllowedOriginError::InvalidAllowedOrigin), + "unexpected allowed origin: {invalid:?}" + ); + } +} + +#[test] +fn native_messaging_manifest_interface_is_exactly_stdio() { + let interface = NativeMessagingInterfaceType::parse("stdio").expect("stdio is the Chrome type"); + assert_eq!(interface.as_str(), "stdio"); + + for invalid in ["", "STDIO", "stdio ", "pipe"] { + assert_eq!( + NativeMessagingInterfaceType::parse(invalid), + Err(NativeMessagingInterfaceTypeError::UnsupportedInterfaceType) + ); + } +} + +#[test] +fn native_messaging_manifest_policy_grants_only_exact_listed_extensions() { + let allowed_extension = extension_id(ALLOWED_EXTENSION_ID); + let other_extension = extension_id(OTHER_EXTENSION_ID); + let host = host_name(); + let interface = NativeMessagingInterfaceType::parse("stdio").expect("stdio is valid"); + let origin = allowed_origin(&format!("chrome-extension://{ALLOWED_EXTENSION_ID}/")); + + let policy = NativeMessagingManifestAccessPolicy::new( + host.clone(), + interface, + vec![origin.clone()], + ) + .expect("non-empty exact allow-list is valid"); + + assert_eq!(policy.host_name(), &host); + assert_eq!(policy.interface_type(), interface); + assert_eq!(policy.allowed_origins(), &[origin]); + + let grant = policy + .grant_for(&allowed_extension) + .expect("listed extension receives an exact host grant"); + assert_eq!(grant.extension_id(), &allowed_extension); + assert_eq!(grant.host_name(), &host); + assert!(policy.grant_for(&other_extension).is_none()); +} + +#[test] +fn native_messaging_manifest_policy_rejects_empty_and_duplicate_allow_lists() { + let interface = NativeMessagingInterfaceType::parse("stdio").expect("stdio is valid"); + assert_eq!( + NativeMessagingManifestAccessPolicy::new(host_name(), interface, Vec::new()), + Err(NativeMessagingManifestAccessPolicyError::MissingAllowedOrigin) + ); + + let origin = allowed_origin(&format!("chrome-extension://{ALLOWED_EXTENSION_ID}/")); + assert_eq!( + NativeMessagingManifestAccessPolicy::new( + host_name(), + interface, + vec![origin.clone(), origin], + ), + Err(NativeMessagingManifestAccessPolicyError::DuplicateAllowedOrigin) + ); +} + +#[test] +fn native_messaging_manifest_policy_errors_are_stable_and_source_free() { + for (message, error) in [ + ( + "native-messaging allowed origin must be one exact chrome-extension origin", + NativeMessagingAllowedOriginError::InvalidAllowedOrigin.to_string(), + ), + ( + "native-messaging manifest interface type must be stdio", + NativeMessagingInterfaceTypeError::UnsupportedInterfaceType.to_string(), + ), + ( + "native-messaging manifest allowed_origins must contain at least one exact extension", + NativeMessagingManifestAccessPolicyError::MissingAllowedOrigin.to_string(), + ), + ( + "native-messaging manifest allowed_origins must not repeat an extension", + NativeMessagingManifestAccessPolicyError::DuplicateAllowedOrigin.to_string(), + ), + ] { + assert_eq!(error, message); + } + + assert!(Error::source(&NativeMessagingAllowedOriginError::InvalidAllowedOrigin).is_none()); + assert!( + Error::source(&NativeMessagingInterfaceTypeError::UnsupportedInterfaceType).is_none() + ); + assert!(Error::source(&NativeMessagingManifestAccessPolicyError::MissingAllowedOrigin).is_none()); + assert!(Error::source(&NativeMessagingManifestAccessPolicyError::DuplicateAllowedOrigin).is_none()); +} From 5c50ab527214b483b00945388761b280f81848d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:15:33 -0700 Subject: [PATCH 18/25] test(native-messaging): defer manifest policy to canonical descendant --- .../tests/native_messaging_manifest_policy.rs | 138 ------------------ 1 file changed, 138 deletions(-) delete mode 100644 crates/originweave-core/tests/native_messaging_manifest_policy.rs diff --git a/crates/originweave-core/tests/native_messaging_manifest_policy.rs b/crates/originweave-core/tests/native_messaging_manifest_policy.rs deleted file mode 100644 index d14214963..000000000 --- a/crates/originweave-core/tests/native_messaging_manifest_policy.rs +++ /dev/null @@ -1,138 +0,0 @@ -#![allow(clippy::expect_used)] - -use std::error::Error; - -use originweave_core::{ - ExtensionId, NativeMessagingAllowedOrigin, NativeMessagingAllowedOriginError, - NativeMessagingHostName, NativeMessagingInterfaceType, NativeMessagingInterfaceTypeError, - NativeMessagingManifestAccessPolicy, NativeMessagingManifestAccessPolicyError, -}; - -const ALLOWED_EXTENSION_ID: &str = "abcdefghijklmnopabcdefghijklmnop"; -const OTHER_EXTENSION_ID: &str = "bcdefghijklmnopabcdefghijklmnopa"; - -fn extension_id(value: &str) -> ExtensionId { - ExtensionId::parse(value).expect("valid extension id") -} - -fn allowed_origin(value: &str) -> NativeMessagingAllowedOrigin { - NativeMessagingAllowedOrigin::parse(value).expect("valid allowed origin") -} - -fn host_name() -> NativeMessagingHostName { - NativeMessagingHostName::parse("com.contextualwisdom.originweave") - .expect("valid native messaging host name") -} - -#[test] -fn native_messaging_allowed_origin_requires_one_exact_chrome_extension_origin() { - let raw = format!("chrome-extension://{ALLOWED_EXTENSION_ID}/"); - let origin = allowed_origin(&raw); - - assert_eq!(origin.extension_id(), &extension_id(ALLOWED_EXTENSION_ID)); - assert_eq!(origin.as_str(), raw); - - for invalid in [ - "chrome-extension://*/", - "chrome-extension://abcdefghijklmnopabcdefghijklmnop", - "chrome-extension://abcdefghijklmnopabcdefghijklmnop/path", - "https://abcdefghijklmnopabcdefghijklmnop/", - "chrome-extension://ABCDEFGHIJKLMNOPABCDEFGHIJKLMNOP/", - ] { - assert_eq!( - NativeMessagingAllowedOrigin::parse(invalid), - Err(NativeMessagingAllowedOriginError::InvalidAllowedOrigin), - "unexpected allowed origin: {invalid:?}" - ); - } -} - -#[test] -fn native_messaging_manifest_interface_is_exactly_stdio() { - let interface = NativeMessagingInterfaceType::parse("stdio").expect("stdio is the Chrome type"); - assert_eq!(interface.as_str(), "stdio"); - - for invalid in ["", "STDIO", "stdio ", "pipe"] { - assert_eq!( - NativeMessagingInterfaceType::parse(invalid), - Err(NativeMessagingInterfaceTypeError::UnsupportedInterfaceType) - ); - } -} - -#[test] -fn native_messaging_manifest_policy_grants_only_exact_listed_extensions() { - let allowed_extension = extension_id(ALLOWED_EXTENSION_ID); - let other_extension = extension_id(OTHER_EXTENSION_ID); - let host = host_name(); - let interface = NativeMessagingInterfaceType::parse("stdio").expect("stdio is valid"); - let origin = allowed_origin(&format!("chrome-extension://{ALLOWED_EXTENSION_ID}/")); - - let policy = NativeMessagingManifestAccessPolicy::new( - host.clone(), - interface, - vec![origin.clone()], - ) - .expect("non-empty exact allow-list is valid"); - - assert_eq!(policy.host_name(), &host); - assert_eq!(policy.interface_type(), interface); - assert_eq!(policy.allowed_origins(), &[origin]); - - let grant = policy - .grant_for(&allowed_extension) - .expect("listed extension receives an exact host grant"); - assert_eq!(grant.extension_id(), &allowed_extension); - assert_eq!(grant.host_name(), &host); - assert!(policy.grant_for(&other_extension).is_none()); -} - -#[test] -fn native_messaging_manifest_policy_rejects_empty_and_duplicate_allow_lists() { - let interface = NativeMessagingInterfaceType::parse("stdio").expect("stdio is valid"); - assert_eq!( - NativeMessagingManifestAccessPolicy::new(host_name(), interface, Vec::new()), - Err(NativeMessagingManifestAccessPolicyError::MissingAllowedOrigin) - ); - - let origin = allowed_origin(&format!("chrome-extension://{ALLOWED_EXTENSION_ID}/")); - assert_eq!( - NativeMessagingManifestAccessPolicy::new( - host_name(), - interface, - vec![origin.clone(), origin], - ), - Err(NativeMessagingManifestAccessPolicyError::DuplicateAllowedOrigin) - ); -} - -#[test] -fn native_messaging_manifest_policy_errors_are_stable_and_source_free() { - for (message, error) in [ - ( - "native-messaging allowed origin must be one exact chrome-extension origin", - NativeMessagingAllowedOriginError::InvalidAllowedOrigin.to_string(), - ), - ( - "native-messaging manifest interface type must be stdio", - NativeMessagingInterfaceTypeError::UnsupportedInterfaceType.to_string(), - ), - ( - "native-messaging manifest allowed_origins must contain at least one exact extension", - NativeMessagingManifestAccessPolicyError::MissingAllowedOrigin.to_string(), - ), - ( - "native-messaging manifest allowed_origins must not repeat an extension", - NativeMessagingManifestAccessPolicyError::DuplicateAllowedOrigin.to_string(), - ), - ] { - assert_eq!(error, message); - } - - assert!(Error::source(&NativeMessagingAllowedOriginError::InvalidAllowedOrigin).is_none()); - assert!( - Error::source(&NativeMessagingInterfaceTypeError::UnsupportedInterfaceType).is_none() - ); - assert!(Error::source(&NativeMessagingManifestAccessPolicyError::MissingAllowedOrigin).is_none()); - assert!(Error::source(&NativeMessagingManifestAccessPolicyError::DuplicateAllowedOrigin).is_none()); -} From 9652e624b0bb53d39c15686f27659f8fe7d6fa43 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 07:43:42 -0700 Subject: [PATCH 19/25] test(docs): pin full Chromium native-messaging source identity --- tests/test_doctoring_reference_contract.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/test_doctoring_reference_contract.py b/tests/test_doctoring_reference_contract.py index a805778c4..a0583f50d 100644 --- a/tests/test_doctoring_reference_contract.py +++ b/tests/test_doctoring_reference_contract.py @@ -38,15 +38,21 @@ def test_native_messaging_decision_trace_is_pinned(self) -> None: ): with self.subTest(expected=expected): self.assertIn(expected, text) - self.assertIn( - "https://chromium.googlesource.com/chromium/src/+/160af61f9d1316fd1f1dc41e9503cc1f1926d31f/", - compatibility, + expected_source_url = ( + "https://chromium.googlesource.com/chromium/src/+/" + "160af61f9d1316fd1f1dc41e9503cc1f1926d31f/" + "chrome/browser/extensions/api/messaging/native_message_process_host.cc" ) - self.assertNotIn( + expected_blob = "9d205a90d70b0c1c9f0b3b1c5f296528f6b21755" + mutable_source_url = ( "https://chromium.googlesource.com/chromium/src/+/refs/heads/main/" - "chrome/browser/extensions/api/messaging/native_message_process_host.cc", - compatibility, + "chrome/browser/extensions/api/messaging/native_message_process_host.cc" ) + for document in (text, compatibility): + with self.subTest(document=document[:24]): + self.assertIn(expected_source_url, document) + self.assertIn(expected_blob, document) + self.assertNotIn(mutable_source_url, document) if __name__ == "__main__": From f850eb444d231d1d0c86fa7d87c771a3384142a7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:12:23 +0900 Subject: [PATCH 20/25] test(ddd): require extension adapter bounded context --- tests/test_extension_context_architecture.py | 39 ++++++++++++++++++++ 1 file changed, 39 insertions(+) create mode 100644 tests/test_extension_context_architecture.py diff --git a/tests/test_extension_context_architecture.py b/tests/test_extension_context_architecture.py new file mode 100644 index 000000000..3dc363130 --- /dev/null +++ b/tests/test_extension_context_architecture.py @@ -0,0 +1,39 @@ +"""Architectural fitness for the Extension Policy bounded context.""" + +from __future__ import annotations + +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class ExtensionContextArchitectureTest(unittest.TestCase): + """Keep Chromium extension adapter vocabulary out of stable browser authority contracts.""" + + def test_native_messaging_adapter_has_dedicated_context(self) -> None: + """Native-messaging integration belongs to the Extension Policy context, not core.""" + workspace = (ROOT / "Cargo.toml").read_text(encoding="utf-8") + self.assertIn('"crates/originweave-extension"', workspace) + + extension_root = ROOT / "crates" / "originweave-extension" + self.assertTrue((extension_root / "Cargo.toml").is_file()) + self.assertTrue((extension_root / "src" / "lib.rs").is_file()) + self.assertTrue((extension_root / "src" / "native_messaging.rs").is_file()) + + core_root = ROOT / "crates" / "originweave-core" / "src" + self.assertFalse((core_root / "native_messaging.rs").exists()) + core_entry = (core_root / "root.rs").read_text(encoding="utf-8") + self.assertNotIn("mod native_messaging;", core_entry) + + extension_manifest = (extension_root / "Cargo.toml").read_text(encoding="utf-8") + self.assertIn('originweave-core = { path = "../originweave-core" }', extension_manifest) + core_manifest = (ROOT / "crates" / "originweave-core" / "Cargo.toml").read_text( + encoding="utf-8" + ) + self.assertNotIn("originweave-extension", core_manifest) + + +if __name__ == "__main__": + unittest.main() From ca2ea5ac92b26e362d4100d32836c02cf9e1a01e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:16:58 +0900 Subject: [PATCH 21/25] refactor(ddd): isolate extension adapter context --- Cargo.toml | 1 + crates/originweave-core/src/root.rs | 2 -- crates/originweave-extension/Cargo.toml | 20 +++++++++++++++++++ crates/originweave-extension/src/lib.rs | 13 ++++++++++++ .../src/native_messaging.rs | 0 .../tests/native_messaging_authority.rs | 8 +++++--- .../tests/native_messaging_framing.rs | 2 ++ .../tests/native_messaging_host_bounds.rs | 2 ++ .../support}/native_messaging_framing.rs | 0 .../support}/native_messaging_host_bounds.rs | 0 10 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 crates/originweave-extension/Cargo.toml create mode 100644 crates/originweave-extension/src/lib.rs rename crates/{originweave-core => originweave-extension}/src/native_messaging.rs (100%) rename crates/{originweave-core => originweave-extension}/tests/native_messaging_authority.rs (93%) create mode 100644 crates/originweave-extension/tests/native_messaging_framing.rs create mode 100644 crates/originweave-extension/tests/native_messaging_host_bounds.rs rename crates/{originweave-core/tests => originweave-extension/tests/support}/native_messaging_framing.rs (100%) rename crates/{originweave-core/tests => originweave-extension/tests/support}/native_messaging_host_bounds.rs (100%) diff --git a/Cargo.toml b/Cargo.toml index 0d5ab469c..7c7a78e49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/originweave-core", + "crates/originweave-extension", "crates/originweave-bap", "crates/originweave-policy", "crates/originweave-resource", diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs index 9c3018431..c47a136d4 100644 --- a/crates/originweave-core/src/root.rs +++ b/crates/originweave-core/src/root.rs @@ -13,7 +13,5 @@ pub use contracts::*; /// Stateless MCP routing validation that maps only explicit tools to typed actions. pub mod mcp; -mod native_messaging; -pub use native_messaging::*; /// Deterministic fail-closed release benchmark acceptance aggregation. pub mod release_acceptance; diff --git a/crates/originweave-extension/Cargo.toml b/crates/originweave-extension/Cargo.toml new file mode 100644 index 000000000..65b68bac5 --- /dev/null +++ b/crates/originweave-extension/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "originweave-extension" +description = "OriginWeave Extension Policy bounded context and Chromium extension adapter contracts." +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +authors.workspace = true +repository.workspace = true +homepage.workspace = true +publish = false + +[lib] +path = "src/lib.rs" + +[dependencies] +originweave-core = { path = "../originweave-core" } + +[lints] +workspace = true diff --git a/crates/originweave-extension/src/lib.rs b/crates/originweave-extension/src/lib.rs new file mode 100644 index 000000000..000e93c79 --- /dev/null +++ b/crates/originweave-extension/src/lib.rs @@ -0,0 +1,13 @@ +//! Extension Policy bounded context for Chromium-compatible extension integration. +//! +//! This crate owns Chrome-specific extension adapter vocabulary and authority checks. +//! Stable browser/session/action contracts remain in `originweave-core`; this context +//! depends inward on those contracts without exporting Chromium adapter types back into core. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +use originweave_core::ExtensionId; + +mod native_messaging; +pub use native_messaging::*; diff --git a/crates/originweave-core/src/native_messaging.rs b/crates/originweave-extension/src/native_messaging.rs similarity index 100% rename from crates/originweave-core/src/native_messaging.rs rename to crates/originweave-extension/src/native_messaging.rs diff --git a/crates/originweave-core/tests/native_messaging_authority.rs b/crates/originweave-extension/tests/native_messaging_authority.rs similarity index 93% rename from crates/originweave-core/tests/native_messaging_authority.rs rename to crates/originweave-extension/tests/native_messaging_authority.rs index 014930b32..7c190595c 100644 --- a/crates/originweave-core/tests/native_messaging_authority.rs +++ b/crates/originweave-extension/tests/native_messaging_authority.rs @@ -2,9 +2,11 @@ use originweave_core::{ BrowserSessionId, BrowsingContextId, ExtensionAccessDecision, ExtensionAccessRequest, - ExtensionAgentCapability, ExtensionId, NativeMessagingAccessDecision, - NativeMessagingAccessRequest, NativeMessagingHostGrant, NativeMessagingHostName, Origin, - evaluate_extension_access, evaluate_native_messaging_access, + ExtensionAgentCapability, ExtensionId, Origin, evaluate_extension_access, +}; +use originweave_extension::{ + NativeMessagingAccessDecision, NativeMessagingAccessRequest, NativeMessagingHostGrant, + NativeMessagingHostName, evaluate_native_messaging_access, }; const UNEXPIRED_NOW_EPOCH_SECONDS: u64 = 1_700_000_000; diff --git a/crates/originweave-extension/tests/native_messaging_framing.rs b/crates/originweave-extension/tests/native_messaging_framing.rs new file mode 100644 index 000000000..da69e7446 --- /dev/null +++ b/crates/originweave-extension/tests/native_messaging_framing.rs @@ -0,0 +1,2 @@ +use originweave_extension as originweave_core; +include!("support/native_messaging_framing.rs"); diff --git a/crates/originweave-extension/tests/native_messaging_host_bounds.rs b/crates/originweave-extension/tests/native_messaging_host_bounds.rs new file mode 100644 index 000000000..933f2590c --- /dev/null +++ b/crates/originweave-extension/tests/native_messaging_host_bounds.rs @@ -0,0 +1,2 @@ +use originweave_extension as originweave_core; +include!("support/native_messaging_host_bounds.rs"); diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-extension/tests/support/native_messaging_framing.rs similarity index 100% rename from crates/originweave-core/tests/native_messaging_framing.rs rename to crates/originweave-extension/tests/support/native_messaging_framing.rs diff --git a/crates/originweave-core/tests/native_messaging_host_bounds.rs b/crates/originweave-extension/tests/support/native_messaging_host_bounds.rs similarity index 100% rename from crates/originweave-core/tests/native_messaging_host_bounds.rs rename to crates/originweave-extension/tests/support/native_messaging_host_bounds.rs From 1ca7ca915b8ffb3def82e4d5ccee952160076076 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:19:16 +0900 Subject: [PATCH 22/25] test(ddd): register extension bounded context --- tests/test_repository_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_repository_contract.py b/tests/test_repository_contract.py index 057a0011b..0db84ef69 100644 --- a/tests/test_repository_contract.py +++ b/tests/test_repository_contract.py @@ -20,6 +20,7 @@ def test_workspace_declares_all_independently_reusable_crates(self) -> None: set(data["workspace"]["members"]), { "crates/originweave-core", + "crates/originweave-extension", "crates/originweave-bap", "crates/originweave-policy", "crates/originweave-destination", From 05723577075afbd15cc25ab0b0a6a52470bb0b65 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:21:41 +0900 Subject: [PATCH 23/25] build: lock extension bounded context --- Cargo.lock | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 848cb7320..114919f93 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,7 +91,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "59ed5838eebb26a2bb2e091bf8a8b4dccdc6d17f656fb07896ee72867612f2" dependencies = [ "libc", ] @@ -173,7 +173,7 @@ dependencies = [ name = "getrandom" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "ff2abc00be7a70a3e8f2e980e0ee05b6776d621bcddc5" dependencies = [ "cfg-if", "libc", @@ -274,6 +274,13 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "originweave-extension" +version = "0.1.0" +dependencies = [ + "originweave-core", +] + [[package]] name = "originweave-destination" version = "0.1.0" From 12360a661f45fcc5fe079374cb8eb8c0c38fdb94 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 15:25:02 +0900 Subject: [PATCH 24/25] fix(build): restore pinned registry checksums --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 114919f93..c5cbc5057 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,7 +91,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e091bf8a8b4dccdc6d17f656fb07896ee72867612f2" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -173,7 +173,7 @@ dependencies = [ name = "getrandom" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff2abc00be7a70a3e8f2e980e0ee05b6776d621bcddc5" +checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "cfg-if", "libc", From 90c4e9a8f31eb94eb46243343160d3f6c96921ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 19:22:30 +0900 Subject: [PATCH 25/25] test(extension): keep moved host bounds lint-clean --- .../tests/support/native_messaging_host_bounds.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/originweave-extension/tests/support/native_messaging_host_bounds.rs b/crates/originweave-extension/tests/support/native_messaging_host_bounds.rs index ed2fd0f13..d900f89a5 100644 --- a/crates/originweave-extension/tests/support/native_messaging_host_bounds.rs +++ b/crates/originweave-extension/tests/support/native_messaging_host_bounds.rs @@ -1,15 +1,12 @@ -#![allow(clippy::expect_used)] - use originweave_core::{NativeMessagingHostName, NativeMessagingHostNameError}; #[test] fn native_messaging_host_name_is_bounded_before_it_becomes_authority() { let exact_limit = "a".repeat(256); + let parsed = NativeMessagingHostName::parse(&exact_limit); assert_eq!( - NativeMessagingHostName::parse(&exact_limit) - .expect("the exact local authority bound remains accepted") - .as_str(), - exact_limit + parsed.as_ref().map(|host| host.as_str()), + Ok(exact_limit.as_str()) ); let one_over = "a".repeat(257);