From 09185819b8c56448ad3e1fe8e0efd3dcf31d55c1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 20:40:25 +0900 Subject: [PATCH 01/27] test(extension): define native messaging framing boundary --- .../tests/native_messaging_framing.rs | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_framing.rs 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..fd68405c5 --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -0,0 +1,119 @@ +#![allow(clippy::expect_used)] + +use std::error::Error; + +use originweave_core::{ + NativeMessagingFrameDirection, NativeMessagingFrameError, decode_native_messaging_frame, + encode_native_messaging_frame, native_messaging_payload_limit, +}; + +const HOST_TO_BROWSER_LIMIT: usize = 1_048_576; +const BROWSER_TO_HOST_LIMIT: usize = 67_108_864; + +#[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() { + let payload = b"{}"; + + for direction in [ + NativeMessagingFrameDirection::HostToBrowser, + NativeMessagingFrameDirection::BrowserToHost, + ] { + let frame = encode_native_messaging_frame(direction, payload) + .expect("small native messaging payload must be frameable"); + assert_eq!(&frame[..4], &2_u32.to_ne_bytes()); + assert_eq!( + decode_native_messaging_frame(direction, &frame) + .expect("freshly encoded native messaging frame must decode"), + payload + ); + } +} + +#[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", + ), + ] { + assert_eq!(error.to_string(), message); + assert!(Error::source(&error).is_none()); + } +} From b65373baac7cc109c23a17f56734f54577732bf2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 20:44:38 +0900 Subject: [PATCH 02/27] test(extension): canonicalize framing RED --- .../originweave-core/tests/native_messaging_framing.rs | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index fd68405c5..c5aeab70b 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -57,7 +57,10 @@ fn native_messaging_encoder_rejects_oversized_host_payload_before_framing() { #[test] fn native_messaging_decoder_rejects_missing_oversized_and_mismatched_lengths() { assert_eq!( - decode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, &[0, 0, 0]), + decode_native_messaging_frame( + NativeMessagingFrameDirection::HostToBrowser, + &[0, 0, 0], + ), Err(NativeMessagingFrameError::MissingLengthPrefix) ); @@ -82,7 +85,10 @@ fn native_messaging_decoder_rejects_missing_oversized_and_mismatched_lengths() { 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), + decode_native_messaging_frame( + NativeMessagingFrameDirection::HostToBrowser, + &short_frame, + ), Err(NativeMessagingFrameError::LengthMismatch) ); From cdf290eed42e3d2cf1d3c8f9532f7f97452c6509 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 20:47:02 +0900 Subject: [PATCH 03/27] test(extension): apply canonical rustfmt to framing RED --- .../tests/native_messaging_framing.rs | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index c5aeab70b..1040c6d31 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -46,10 +46,7 @@ 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, - ), + encode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, &oversized,), Err(NativeMessagingFrameError::PayloadTooLarge) ); } @@ -57,10 +54,7 @@ fn native_messaging_encoder_rejects_oversized_host_payload_before_framing() { #[test] fn native_messaging_decoder_rejects_missing_oversized_and_mismatched_lengths() { assert_eq!( - decode_native_messaging_frame( - NativeMessagingFrameDirection::HostToBrowser, - &[0, 0, 0], - ), + decode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, &[0, 0, 0],), Err(NativeMessagingFrameError::MissingLengthPrefix) ); @@ -85,10 +79,7 @@ fn native_messaging_decoder_rejects_missing_oversized_and_mismatched_lengths() { 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, - ), + decode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, &short_frame,), Err(NativeMessagingFrameError::LengthMismatch) ); From b5ec0ae0abe00b3d051f0a868e57f883eb13829c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:12:51 +0900 Subject: [PATCH 04/27] feat(extension): implement bounded native messaging framing --- crates/originweave-core/src/lib.rs | 93 +++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index 753034070..d35d21213 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -410,7 +410,7 @@ pub enum NodeHandleError { BrowsingContextMismatch { /// Context that originally produced the node handle. observed: BrowsingContextId, - /// Context currently active for the requested action. + /// Context currently active for the browser context. current: BrowsingContextId, }, /// The browser context is now at a different canonical origin. @@ -1181,3 +1181,94 @@ pub fn evaluate_native_messaging_access( } NativeMessagingAccessDecision::Allow } + +const HOST_TO_BROWSER_NATIVE_MESSAGING_LIMIT: usize = 1_048_576; +const BROWSER_TO_HOST_NATIVE_MESSAGING_LIMIT: usize = 67_108_864; + +/// 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, +} + +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") + } + } + } +} + +impl std::error::Error for NativeMessagingFrameError {} + +/// Return Chrome's 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, + } +} + +/// 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 +/// Chrome 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..]) +} From 9e0bdb05bfbb023f9739e41f5c877776eae7cb14 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:20:20 +0900 Subject: [PATCH 05/27] style(extension): apply canonical rustfmt to framing boundary --- crates/originweave-core/src/lib.rs | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index d35d21213..cfedb576b 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -410,7 +410,7 @@ pub enum NodeHandleError { BrowsingContextMismatch { /// Context that originally produced the node handle. observed: BrowsingContextId, - /// Context currently active for the browser context. + /// Context currently active for the requested action. current: BrowsingContextId, }, /// The browser context is now at a different canonical origin. @@ -1208,10 +1208,12 @@ pub enum NativeMessagingFrameError { 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::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") } @@ -1261,8 +1263,7 @@ pub fn decode_native_messaging_frame( return Err(NativeMessagingFrameError::MissingLengthPrefix); } - let advertised_length = - u32::from_ne_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize; + 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); } From bd7066ccf6cd118a1fe2c48a2d32404a7456fe41 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:27:54 +0900 Subject: [PATCH 06/27] docs(extension): record native messaging framing boundary --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4bd39ca..4930ac9c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. +- 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, and fail-closed rejection of oversized, truncated, or trailing frame data without granting Agent authority. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. @@ -56,7 +57,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - Resolver answers are rejected when empty or larger than 256 addresses, preventing an unbounded resolver response from entering policy state. - `localhost` may approve only loopback addresses, while literal IPv4 and IPv6 origins may approve only the exact canonical address encoded in the origin. - Resolver answers must remain a non-empty subset of the origin-bound approved address set; any newly introduced address fails closed as a possible DNS-rebinding event. -- Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target cycle state, and hop capacity before policy state changes. +- Every redirect rechecks target-origin authority, target-bound resolution, HTTPS downgrade, complete-target digest, and hop capacity before policy state changes. - Direct TCP plans reject port zero, zero or excessive timeouts, excessive attempts, unapproved IPs, non-canonical IPv4-mapped IPv6 sockets, and IPv6 flow or scope metadata not represented in destination authority before connection I/O. - Direct connection code accepts only an explicit `SocketAddr`, never a hostname, and does not read proxy environment variables. - Established streams are discarded when peer inspection fails or the observed remote IP or port differs from the approved socket. From 25242e8a632a830545eb83aba4860d638b21c467 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 14 Aug 2026 22:29:02 +0900 Subject: [PATCH 07/27] docs(extension): doctor native messaging framing contract --- docs/doctoring/mv3-compatibility.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/doctoring/mv3-compatibility.md b/docs/doctoring/mv3-compatibility.md index 571c49329..168bcb785 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-14 - **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, but neither is real pinned-Chromium native-host compatibility evidence. | Process launch/registration ownership, JSON/UTF-8 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,10 @@ 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 current native-messaging documentation defines a separate native-host process communicating over `stdin`/`stdout`; each JSON message is UTF-8 encoded and preceded by a 32-bit message length in native byte order. Chrome caps a message sent by the native host to the browser at 1 MB and a message sent by the browser to the native host at 64 MiB. Draft PR #154 implements only this bounded binary framing/resource boundary in reusable Rust: it rejects oversized encoder input before allocation, rejects an oversized advertised decoder length before payload slicing, and requires the complete frame length to equal the advertised byte count so truncation and trailing data fail closed. This does not parse or trust JSON, 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 +68,8 @@ 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 14, 2026, from https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging + 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/ From b7a17e2903b392bbedaa16d17825607143bcfa2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 17:23:56 +0900 Subject: [PATCH 08/27] test(extension): remove panic-based native framing assertions --- .../tests/native_messaging_framing.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index 1040c6d31..d1a197c8f 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -1,5 +1,3 @@ -#![allow(clippy::expect_used)] - use std::error::Error; use originweave_core::{ @@ -23,22 +21,18 @@ fn native_messaging_payload_limits_are_direction_specific() { } #[test] -fn native_messaging_frame_round_trip_uses_native_u32_byte_length() { +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) - .expect("small native messaging payload must be frameable"); + 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) - .expect("freshly encoded native messaging frame must decode"), - payload - ); + assert_eq!(decode_native_messaging_frame(direction, &frame)?, payload); } + Ok(()) } #[test] From 6999fac1042cf41fa9c08be22edac1ccc9c5040c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 21:39:08 +0900 Subject: [PATCH 09/27] test(extension): require UTF-8 native messaging payloads --- .../tests/native_messaging_framing.rs | 42 ++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index d1a197c8f..9ea0db519 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -2,7 +2,7 @@ use std::error::Error; use originweave_core::{ NativeMessagingFrameDirection, NativeMessagingFrameError, decode_native_messaging_frame, - encode_native_messaging_frame, native_messaging_payload_limit, + decode_native_messaging_text_frame, encode_native_messaging_frame, native_messaging_payload_limit, }; const HOST_TO_BROWSER_LIMIT: usize = 1_048_576; @@ -35,6 +35,42 @@ fn native_messaging_frame_round_trip_uses_native_u32_byte_length() -> Result<(), Ok(()) } +#[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]; @@ -103,6 +139,10 @@ fn native_messaging_frame_errors_are_stable_and_source_free() { 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()); From 6103893de11bb974936c18e484e23dc1b270228c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 21:46:40 +0900 Subject: [PATCH 10/27] feat(extension): validate native messaging UTF-8 --- crates/originweave-core/src/lib.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/crates/originweave-core/src/lib.rs b/crates/originweave-core/src/lib.rs index cfedb576b..a122ab0bf 100644 --- a/crates/originweave-core/src/lib.rs +++ b/crates/originweave-core/src/lib.rs @@ -1203,6 +1203,8 @@ pub enum NativeMessagingFrameError { 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 { @@ -1217,6 +1219,9 @@ impl fmt::Display for NativeMessagingFrameError { 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") + } } } } @@ -1273,3 +1278,15 @@ pub fn decode_native_messaging_frame( 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) +} From 9f39778cb54e55bff1a3d2e557cdd26246b0c1e7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 21:47:15 +0900 Subject: [PATCH 11/27] style(extension): apply canonical rustfmt --- .../tests/native_messaging_framing.rs | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index 9ea0db519..0a4ee0a4b 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -2,7 +2,8 @@ use std::error::Error; use originweave_core::{ NativeMessagingFrameDirection, NativeMessagingFrameError, decode_native_messaging_frame, - decode_native_messaging_text_frame, encode_native_messaging_frame, native_messaging_payload_limit, + decode_native_messaging_text_frame, encode_native_messaging_frame, + native_messaging_payload_limit, }; const HOST_TO_BROWSER_LIMIT: usize = 1_048_576; @@ -36,12 +37,11 @@ fn native_messaging_frame_round_trip_uses_native_u32_byte_length() -> Result<(), } #[test] -fn native_messaging_text_frame_accepts_utf8_and_rejects_invalid_text() -> Result<(), Box> { +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, - )?; + let utf8_frame = + encode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, utf8_payload)?; assert_eq!( decode_native_messaging_text_frame( NativeMessagingFrameDirection::HostToBrowser, @@ -50,10 +50,8 @@ fn native_messaging_text_frame_accepts_utf8_and_rejects_invalid_text() -> Result "{\"message\":\"안녕 👋\"}" ); - let invalid_utf8_frame = encode_native_messaging_frame( - NativeMessagingFrameDirection::HostToBrowser, - &[0xff], - )?; + let invalid_utf8_frame = + encode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, &[0xff])?; assert_eq!( decode_native_messaging_text_frame( NativeMessagingFrameDirection::HostToBrowser, From 9b5a3338b89152694ba60d6d1baa804f590115e9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 21:51:35 +0900 Subject: [PATCH 12/27] docs(extension): record native messaging UTF-8 boundary --- docs/doctoring/mv3-compatibility.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/doctoring/mv3-compatibility.md b/docs/doctoring/mv3-compatibility.md index 168bcb785..709716c8c 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-14 +- **Reviewed:** 2026-08-15 - **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** | Active PR #82 defines exact extension-to-host authority and stacked Draft #154 defines bounded binary framing, but neither is real pinned-Chromium native-host compatibility evidence. | Process launch/registration ownership, JSON/UTF-8 parsing, untrusted-message classification, sandboxing, real stdio integration, and executable browser compatibility remain unproven. | +| 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. @@ -48,7 +48,7 @@ Content-script injection and content-script JavaScript isolation are separate co ## Native-messaging protocol boundary -Chrome's current native-messaging documentation defines a separate native-host process communicating over `stdin`/`stdout`; each JSON message is UTF-8 encoded and preceded by a 32-bit message length in native byte order. Chrome caps a message sent by the native host to the browser at 1 MB and a message sent by the browser to the native host at 64 MiB. Draft PR #154 implements only this bounded binary framing/resource boundary in reusable Rust: it rejects oversized encoder input before allocation, rejects an oversized advertised decoder length before payload slicing, and requires the complete frame length to equal the advertised byte count so truncation and trailing data fail closed. This does not parse or trust JSON, launch or authenticate a native-host process, validate operating-system registration, or convert Chrome `nativeMessaging` permission into OriginWeave Agent authority. +Chrome's current native-messaging documentation defines a separate native-host process communicating over `stdin`/`stdout`; each JSON message is UTF-8 encoded and preceded by a 32-bit message length in native byte order. Chrome caps a message sent by the native host to the browser at 1 MB and a message sent by the browser to the native host at 64 MiB. Draft PR #154 implements the bounded binary framing/resource boundary in reusable Rust and now exposes a fail-closed UTF-8 decode boundary: it rejects oversized encoder input before allocation, rejects an oversized advertised decoder length before payload slicing, 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 From 9a84fac9e412c2dec77144b02690b1056ed6e096 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 15 Aug 2026 21:53:42 +0900 Subject: [PATCH 13/27] docs(extension): record UTF-8 framing change --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4930ac9c3..1e9eecc2b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,7 +7,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules. -- 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, and fail-closed rejection of oversized, truncated, or trailing frame data without granting 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, 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. - Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. From 5a66d3687092aedcd83bb04ae06b0051087e2892 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 05:16:28 +0900 Subject: [PATCH 14/27] test(extension): align native messaging identity contract --- 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 93c9bb0635e11098588cc1faaeac6a3274db0b45 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 02:22:39 -0700 Subject: [PATCH 15/27] merge(parent): preserve native messaging authority changelog --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12946290b..ea32e306e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,8 @@ 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. - 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, 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. - 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 fcaa5177d06bc419cfacedd9b60dddd2d8312d2f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 02:23:29 -0700 Subject: [PATCH 16/27] docs(changelog): correct extension origin binding text --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea32e306e..0530e62f1 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. - 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, 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. From fd5630a2dc12205927f1f07b1034e63c9f432fb8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 03:12:22 -0700 Subject: [PATCH 17/27] docs(core): distinguish native messaging resource limits --- crates/originweave-core/src/native_messaging.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/native_messaging.rs b/crates/originweave-core/src/native_messaging.rs index 7df5ff961..ea7b33fba 100644 --- a/crates/originweave-core/src/native_messaging.rs +++ b/crates/originweave-core/src/native_messaging.rs @@ -203,7 +203,7 @@ impl fmt::Display for NativeMessagingFrameError { impl std::error::Error for NativeMessagingFrameError {} -/// Return Chrome's payload ceiling for one native-messaging direction. +/// 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 { @@ -215,7 +215,7 @@ pub const fn native_messaging_payload_limit(direction: NativeMessagingFrameDirec /// 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 -/// Chrome limit. The returned bytes are framing only and carry no trust or Agent authority. +/// 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], From 26b94fa505eaf0eb555ce410f790f2d6bd671c9f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 03:13:20 -0700 Subject: [PATCH 18/27] docs(mv3): correct native messaging size authority --- docs/doctoring/mv3-compatibility.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/doctoring/mv3-compatibility.md b/docs/doctoring/mv3-compatibility.md index bb976ab03..466875a17 100644 --- a/docs/doctoring/mv3-compatibility.md +++ b/docs/doctoring/mv3-compatibility.md @@ -48,7 +48,9 @@ Content-script injection and content-script JavaScript isolation are separate co ## Native-messaging protocol boundary -Chrome's current native-messaging documentation defines a separate native-host process communicating over `stdin`/`stdout`; each JSON message is UTF-8 encoded and preceded by a 32-bit message length in native byte order. Chrome caps a message sent by the native host to the browser at 1 MB and a message sent by the browser to the native host at 64 MiB. Draft PR #154 implements the bounded binary framing/resource boundary in reusable Rust and exposes a fail-closed UTF-8 decode boundary: it rejects oversized encoder input before allocation, rejects an oversized advertised decoder length before payload slicing, 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. +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, 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 @@ -70,6 +72,8 @@ Chrome for Developers. (n.d.). *Manifest file format*. Google. Retrieved August 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`]. Chromium. https://chromium.googlesource.com/chromium/src/+/refs/heads/main/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/ From 75280b98b9d47ca2f4861d5b7be637d726d95f2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 05:16:20 -0700 Subject: [PATCH 19/27] test(extension): require bounded native messaging stream reads --- .../tests/native_messaging_framing.rs | 57 +++++++++++++++++-- 1 file changed, 53 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index 0a4ee0a4b..8b7035fd4 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -1,9 +1,10 @@ use std::error::Error; +use std::io::{Cursor, ErrorKind}; use originweave_core::{ - NativeMessagingFrameDirection, NativeMessagingFrameError, decode_native_messaging_frame, - decode_native_messaging_text_frame, encode_native_messaging_frame, - native_messaging_payload_limit, + 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; @@ -36,6 +37,54 @@ fn native_messaging_frame_round_trip_uses_native_u32_byte_length() -> Result<(), 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_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); + + match read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader) { + Err(NativeMessagingFrameReadError::Io(error)) => { + assert_eq!(error.kind(), ErrorKind::UnexpectedEof); + } + other => panic!("expected typed I/O failure, got {other:?}"), + } +} + #[test] fn native_messaging_text_frame_accepts_utf8_and_rejects_invalid_text() -> Result<(), Box> { @@ -145,4 +194,4 @@ fn native_messaging_frame_errors_are_stable_and_source_free() { assert_eq!(error.to_string(), message); assert!(Error::source(&error).is_none()); } -} +} \ No newline at end of file From 5099732b14d438b533fc23c9a0116e281c942cb1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 05:18:18 -0700 Subject: [PATCH 20/27] test(extension): normalize bounded stream regression formatting --- .../tests/native_messaging_framing.rs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index 8b7035fd4..37dc4618f 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -40,14 +40,12 @@ fn native_messaging_frame_round_trip_uses_native_u32_byte_length() -> Result<(), #[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 frame = + encode_native_messaging_frame(NativeMessagingFrameDirection::HostToBrowser, payload)?; let mut reader = Cursor::new(frame); assert_eq!( - read_native_messaging_payload( - NativeMessagingFrameDirection::HostToBrowser, - &mut reader, - )?, + read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader,)?, payload ); assert_eq!(reader.position(), payload.len() as u64 + 4); @@ -60,10 +58,7 @@ fn native_messaging_stream_reader_rejects_oversized_prefix_before_reading_payloa let mut reader = Cursor::new(oversized); assert!(matches!( - read_native_messaging_payload( - NativeMessagingFrameDirection::HostToBrowser, - &mut reader, - ), + read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader,), Err(NativeMessagingFrameReadError::Frame( NativeMessagingFrameError::PayloadTooLarge )) @@ -194,4 +189,4 @@ fn native_messaging_frame_errors_are_stable_and_source_free() { assert_eq!(error.to_string(), message); assert!(Error::source(&error).is_none()); } -} \ No newline at end of file +} From e8709a4c9ee4ee3957bdefc429db968a5eb2e6d0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 07:28:32 -0700 Subject: [PATCH 21/27] fix(extension): bound native messaging stream reads --- .../originweave-core/src/native_messaging.rs | 58 +++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/crates/originweave-core/src/native_messaging.rs b/crates/originweave-core/src/native_messaging.rs index ea7b33fba..d72f152e8 100644 --- a/crates/originweave-core/src/native_messaging.rs +++ b/crates/originweave-core/src/native_messaging.rs @@ -1,6 +1,7 @@ //! Explicit Chrome native-messaging host authority without ambient Agent authority. use std::fmt; +use std::io::Read; use crate::ExtensionId; @@ -203,6 +204,33 @@ impl fmt::Display for NativeMessagingFrameError { 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 { @@ -212,6 +240,36 @@ pub const fn native_messaging_payload_limit(direction: NativeMessagingFrameDirec } } +/// 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 R, +) -> 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![0_u8; advertised_length]; + reader + .read_exact(&mut payload) + .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 From c1773cf3c4f7291957f77c85b2a3c9a82da6c036 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 07:29:13 -0700 Subject: [PATCH 22/27] test(extension): cover native messaging stream read failures --- .../tests/native_messaging_framing.rs | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index 37dc4618f..9e24e7920 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -66,6 +66,18 @@ fn native_messaging_stream_reader_rejects_oversized_prefix_before_reading_payloa assert_eq!(reader.position(), 4); } +#[test] +fn native_messaging_stream_reader_preserves_truncated_prefix_io_cause() { + let mut reader = Cursor::new([0_u8; 3]); + + match read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader) { + Err(NativeMessagingFrameReadError::Io(error)) => { + assert_eq!(error.kind(), ErrorKind::UnexpectedEof); + } + other => panic!("expected typed prefix I/O failure, got {other:?}"), + } +} + #[test] fn native_messaging_stream_reader_preserves_truncated_payload_io_cause() { let mut frame = Vec::from(4_u32.to_ne_bytes()); @@ -80,6 +92,24 @@ fn native_messaging_stream_reader_preserves_truncated_payload_io_cause() { } } +#[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).expect("stream read errors preserve their I/O source"); + let source = source + .downcast_ref::() + .expect("the preserved source remains the original I/O error type"); + assert_eq!(source.kind(), ErrorKind::UnexpectedEof); +} + #[test] fn native_messaging_text_frame_accepts_utf8_and_rejects_invalid_text() -> Result<(), Box> { From 08642a8c3b95d290b4bc0edb26e185da005c6721 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 07:31:24 -0700 Subject: [PATCH 23/27] style(extension): apply canonical stream-reader formatting --- crates/originweave-core/tests/native_messaging_framing.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index 9e24e7920..3c3233c74 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -94,14 +94,16 @@ fn native_messaging_stream_reader_preserves_truncated_payload_io_cause() { #[test] fn native_messaging_stream_read_errors_preserve_typed_sources_and_display() { - let frame_error = NativeMessagingFrameReadError::Frame(NativeMessagingFrameError::PayloadTooLarge); + 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)); + 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).expect("stream read errors preserve their I/O source"); let source = source From 0d0827d0d0c97d5363e0fd5c07f5df2fb5f6335c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:34:27 -0700 Subject: [PATCH 24/27] test(extension): satisfy strict native messaging error assertions --- .../tests/native_messaging_framing.rs | 37 ++++++++++--------- 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index 3c3233c74..7fd5106d7 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -70,12 +70,11 @@ fn native_messaging_stream_reader_rejects_oversized_prefix_before_reading_payloa fn native_messaging_stream_reader_preserves_truncated_prefix_io_cause() { let mut reader = Cursor::new([0_u8; 3]); - match read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader) { - Err(NativeMessagingFrameReadError::Io(error)) => { - assert_eq!(error.kind(), ErrorKind::UnexpectedEof); - } - other => panic!("expected typed prefix I/O failure, got {other:?}"), - } + assert!(matches!( + read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader), + Err(NativeMessagingFrameReadError::Io(ref error)) + if error.kind() == ErrorKind::UnexpectedEof + )); } #[test] @@ -84,12 +83,11 @@ fn native_messaging_stream_reader_preserves_truncated_payload_io_cause() { frame.extend_from_slice(b"abc"); let mut reader = Cursor::new(frame); - match read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader) { - Err(NativeMessagingFrameReadError::Io(error)) => { - assert_eq!(error.kind(), ErrorKind::UnexpectedEof); - } - other => panic!("expected typed I/O failure, got {other:?}"), - } + assert!(matches!( + read_native_messaging_payload(NativeMessagingFrameDirection::HostToBrowser, &mut reader), + Err(NativeMessagingFrameReadError::Io(ref error)) + if error.kind() == ErrorKind::UnexpectedEof + )); } #[test] @@ -105,11 +103,16 @@ fn native_messaging_stream_read_errors_preserve_typed_sources_and_display() { 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).expect("stream read errors preserve their I/O source"); - let source = source - .downcast_ref::() - .expect("the preserved source remains the original I/O error type"); - assert_eq!(source.kind(), ErrorKind::UnexpectedEof); + 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] From 30c89f91215813435d227d9c049b43c9a8558c6b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 25 Aug 2026 08:35:26 -0700 Subject: [PATCH 25/27] fix(extension): make stream coverage deterministic --- crates/originweave-core/src/native_messaging.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/native_messaging.rs b/crates/originweave-core/src/native_messaging.rs index d72f152e8..0c0d2eae3 100644 --- a/crates/originweave-core/src/native_messaging.rs +++ b/crates/originweave-core/src/native_messaging.rs @@ -247,9 +247,9 @@ pub const fn native_messaging_payload_limit(direction: NativeMessagingFrameDirec /// `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( +pub fn read_native_messaging_payload( direction: NativeMessagingFrameDirection, - reader: &mut R, + reader: &mut dyn Read, ) -> Result, NativeMessagingFrameReadError> { let mut prefix = [0_u8; 4]; reader From 77ef39b293c2f5e527827d88ed07d66588572976 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 14:10:45 +0900 Subject: [PATCH 26/27] docs(mv3): pin native messaging evidence --- CHANGELOG.md | 3 ++- docs/doctoring.md | 8 +++++++ docs/doctoring/mv3-compatibility.md | 4 ++-- tests/test_doctoring_reference_contract.py | 25 ++++++++++++++++++++++ 4 files changed, 37 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 632115065..11516ec6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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, 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. @@ -104,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/docs/doctoring.md b/docs/doctoring.md index ec51daaf3..bfaf528ea 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, 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 466875a17..5bc687ccc 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-24 +- **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**. @@ -72,7 +72,7 @@ Chrome for Developers. (n.d.). *Manifest file format*. Google. Retrieved August 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`]. Chromium. https://chromium.googlesource.com/chromium/src/+/refs/heads/main/chrome/browser/extensions/api/messaging/native_message_process_host.cc +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 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 c02f3fd0e1fbd94a7dafbfdf0aa0192a2d5cdcf4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 28 Aug 2026 15:05:28 +0900 Subject: [PATCH 27/27] fix: bound native messaging stream read buffers --- CHANGELOG.md | 2 +- .../originweave-core/src/native_messaging.rs | 14 ++++-- .../tests/native_messaging_framing.rs | 45 ++++++++++++++++++- docs/doctoring.md | 2 +- docs/doctoring/mv3-compatibility.md | 2 +- 5 files changed, 57 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 11516ec6c..3fc5651f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,7 +17,7 @@ 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, 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. +- 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. diff --git a/crates/originweave-core/src/native_messaging.rs b/crates/originweave-core/src/native_messaging.rs index 0c0d2eae3..5c8823842 100644 --- a/crates/originweave-core/src/native_messaging.rs +++ b/crates/originweave-core/src/native_messaging.rs @@ -8,6 +8,7 @@ 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)] @@ -263,10 +264,15 @@ pub fn read_native_messaging_payload( )); } - let mut payload = vec![0_u8; advertised_length]; - reader - .read_exact(&mut payload) - .map_err(NativeMessagingFrameReadError::Io)?; + 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) } diff --git a/crates/originweave-core/tests/native_messaging_framing.rs b/crates/originweave-core/tests/native_messaging_framing.rs index 7fd5106d7..667ea9587 100644 --- a/crates/originweave-core/tests/native_messaging_framing.rs +++ b/crates/originweave-core/tests/native_messaging_framing.rs @@ -1,5 +1,5 @@ use std::error::Error; -use std::io::{Cursor, ErrorKind}; +use std::io::{Cursor, ErrorKind, Read}; use originweave_core::{ NativeMessagingFrameDirection, NativeMessagingFrameError, NativeMessagingFrameReadError, @@ -10,6 +10,36 @@ use originweave_core::{ 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!( @@ -90,6 +120,19 @@ fn native_messaging_stream_reader_preserves_truncated_payload_io_cause() { )); } +#[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 = diff --git a/docs/doctoring.md b/docs/doctoring.md index bfaf528ea..c0efe6155 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -12,7 +12,7 @@ The final Model Context Protocol `2026-07-28` specification defines the currentl ### 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, 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`. +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. diff --git a/docs/doctoring/mv3-compatibility.md b/docs/doctoring/mv3-compatibility.md index 5bc687ccc..4f658463c 100644 --- a/docs/doctoring/mv3-compatibility.md +++ b/docs/doctoring/mv3-compatibility.md @@ -50,7 +50,7 @@ Content-script injection and content-script JavaScript isolation are separate co 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, 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. +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