From afea70bee5f479fc0b925be154fe79279fc34ae4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 11 Aug 2026 19:35:10 +0900 Subject: [PATCH 1/5] 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 00000000..74d89be3 --- /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 2/5] 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 74d89be3..5bee7744 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 3/5] 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 88dd2e58..a12f43b9 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 4/5] 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 a12f43b9..61260190 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 5/5] 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 5bee7744..f0a830cd 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 [