From c15400327c3991a97abc83e0de6bc32a33e3a2be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 00:59:08 -0700 Subject: [PATCH 1/9] test(extension): require bounded native messaging frame codec --- .../tests/native_messaging_frame_codec.rs | 100 ++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_frame_codec.rs diff --git a/crates/originweave-core/tests/native_messaging_frame_codec.rs b/crates/originweave-core/tests/native_messaging_frame_codec.rs new file mode 100644 index 000000000..088f67719 --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_frame_codec.rs @@ -0,0 +1,100 @@ +#![allow(clippy::expect_used)] + +use originweave_core::{ + NativeMessagingFrameDirection, NativeMessagingFrameError, decode_native_messaging_frame, + encode_native_messaging_frame, +}; + +fn frame_with_declared_length(declared_length: u32, payload: &[u8]) -> Vec { + let mut frame = Vec::with_capacity(4 + payload.len()); + frame.extend_from_slice(&declared_length.to_ne_bytes()); + frame.extend_from_slice(payload); + frame +} + +#[test] +fn native_messaging_frames_use_native_endian_byte_length_and_round_trip_utf8() { + let payload = r#"{"message":"안녕"}"#; + let encoded = encode_native_messaging_frame( + payload, + NativeMessagingFrameDirection::ChromeToHost, + ) + .expect("bounded payload should encode"); + + let declared = u32::from_ne_bytes(encoded[..4].try_into().expect("four-byte header")); + assert_eq!(declared as usize, payload.len()); + assert_eq!(&encoded[4..], payload.as_bytes()); + assert_eq!( + decode_native_messaging_frame( + &encoded, + NativeMessagingFrameDirection::ChromeToHost, + ) + .expect("encoded payload should decode"), + payload + ); +} + +#[test] +fn native_messaging_decode_rejects_truncated_header_and_length_mismatch() { + assert_eq!( + decode_native_messaging_frame(&[0, 0, 0], NativeMessagingFrameDirection::HostToChrome), + Err(NativeMessagingFrameError::TruncatedHeader) + ); + + let truncated = frame_with_declared_length(5, b"{} "); + assert_eq!( + decode_native_messaging_frame( + &truncated, + NativeMessagingFrameDirection::HostToChrome, + ), + Err(NativeMessagingFrameError::LengthMismatch { + declared_bytes: 5, + actual_bytes: 3, + }) + ); + + let trailing = frame_with_declared_length(2, b"{}x"); + assert_eq!( + decode_native_messaging_frame(&trailing, NativeMessagingFrameDirection::HostToChrome), + Err(NativeMessagingFrameError::LengthMismatch { + declared_bytes: 2, + actual_bytes: 3, + }) + ); +} + +#[test] +fn native_messaging_decode_enforces_direction_specific_chrome_limits_before_body_use() { + let host_to_chrome_oversize = frame_with_declared_length(1_048_577, b""); + assert_eq!( + decode_native_messaging_frame( + &host_to_chrome_oversize, + NativeMessagingFrameDirection::HostToChrome, + ), + Err(NativeMessagingFrameError::PayloadTooLarge { + declared_bytes: 1_048_577, + maximum_bytes: 1_048_576, + }) + ); + + let chrome_to_host_oversize = frame_with_declared_length(67_108_865, b""); + assert_eq!( + decode_native_messaging_frame( + &chrome_to_host_oversize, + NativeMessagingFrameDirection::ChromeToHost, + ), + Err(NativeMessagingFrameError::PayloadTooLarge { + declared_bytes: 67_108_865, + maximum_bytes: 67_108_864, + }) + ); +} + +#[test] +fn native_messaging_decode_rejects_non_utf8_payload_without_reflecting_bytes() { + let frame = frame_with_declared_length(2, &[0xff, 0xfe]); + let error = decode_native_messaging_frame(&frame, NativeMessagingFrameDirection::HostToChrome) + .expect_err("non-UTF-8 native message must fail closed"); + assert_eq!(error, NativeMessagingFrameError::InvalidUtf8); + assert_eq!(error.to_string(), "native-messaging payload is not valid UTF-8"); +} From 3d9b8a2f0468848f7f3eeaeb13ac75c0798da0e8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:02:16 -0700 Subject: [PATCH 2/9] test(extension): format native messaging frame RED --- .../tests/native_messaging_frame_codec.rs | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_frame_codec.rs b/crates/originweave-core/tests/native_messaging_frame_codec.rs index 088f67719..9a7341841 100644 --- a/crates/originweave-core/tests/native_messaging_frame_codec.rs +++ b/crates/originweave-core/tests/native_messaging_frame_codec.rs @@ -15,21 +15,16 @@ fn frame_with_declared_length(declared_length: u32, payload: &[u8]) -> Vec { #[test] fn native_messaging_frames_use_native_endian_byte_length_and_round_trip_utf8() { let payload = r#"{"message":"안녕"}"#; - let encoded = encode_native_messaging_frame( - payload, - NativeMessagingFrameDirection::ChromeToHost, - ) - .expect("bounded payload should encode"); + let encoded = + encode_native_messaging_frame(payload, NativeMessagingFrameDirection::ChromeToHost) + .expect("bounded payload should encode"); let declared = u32::from_ne_bytes(encoded[..4].try_into().expect("four-byte header")); assert_eq!(declared as usize, payload.len()); assert_eq!(&encoded[4..], payload.as_bytes()); assert_eq!( - decode_native_messaging_frame( - &encoded, - NativeMessagingFrameDirection::ChromeToHost, - ) - .expect("encoded payload should decode"), + decode_native_messaging_frame(&encoded, NativeMessagingFrameDirection::ChromeToHost,) + .expect("encoded payload should decode"), payload ); } @@ -43,10 +38,7 @@ fn native_messaging_decode_rejects_truncated_header_and_length_mismatch() { let truncated = frame_with_declared_length(5, b"{} "); assert_eq!( - decode_native_messaging_frame( - &truncated, - NativeMessagingFrameDirection::HostToChrome, - ), + decode_native_messaging_frame(&truncated, NativeMessagingFrameDirection::HostToChrome,), Err(NativeMessagingFrameError::LengthMismatch { declared_bytes: 5, actual_bytes: 3, @@ -90,11 +82,37 @@ fn native_messaging_decode_enforces_direction_specific_chrome_limits_before_body ); } +#[test] +fn native_messaging_encode_enforces_host_to_chrome_limit() { + let maximum_payload = "x".repeat(1_048_576); + let encoded = encode_native_messaging_frame( + &maximum_payload, + NativeMessagingFrameDirection::HostToChrome, + ) + .expect("Chrome accepts a native host message at the documented byte limit"); + assert_eq!(encoded.len(), 4 + maximum_payload.len()); + + let oversized_payload = "x".repeat(1_048_577); + assert_eq!( + encode_native_messaging_frame( + &oversized_payload, + NativeMessagingFrameDirection::HostToChrome, + ), + Err(NativeMessagingFrameError::PayloadTooLarge { + declared_bytes: 1_048_577, + maximum_bytes: 1_048_576, + }) + ); +} + #[test] fn native_messaging_decode_rejects_non_utf8_payload_without_reflecting_bytes() { let frame = frame_with_declared_length(2, &[0xff, 0xfe]); let error = decode_native_messaging_frame(&frame, NativeMessagingFrameDirection::HostToChrome) .expect_err("non-UTF-8 native message must fail closed"); assert_eq!(error, NativeMessagingFrameError::InvalidUtf8); - assert_eq!(error.to_string(), "native-messaging payload is not valid UTF-8"); + assert_eq!( + error.to_string(), + "native-messaging payload is not valid UTF-8" + ); } From 072f8d9b50dc1834222f33a7328b7c20bfa505d5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:04:11 -0700 Subject: [PATCH 3/9] feat(extension): implement bounded native messaging frame codec --- .../src/native_messaging_frame.rs | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 crates/originweave-core/src/native_messaging_frame.rs diff --git a/crates/originweave-core/src/native_messaging_frame.rs b/crates/originweave-core/src/native_messaging_frame.rs new file mode 100644 index 000000000..96f7e4ce2 --- /dev/null +++ b/crates/originweave-core/src/native_messaging_frame.rs @@ -0,0 +1,137 @@ +//! Bounded Chrome native-messaging frame encoding and decoding. +//! +//! Chrome native messaging prefixes each UTF-8 JSON message with a 32-bit +//! native-endian byte length. This module validates only that framing boundary; +//! JSON parsing, host registration, process launch, authority, and secret handling +//! remain separate reviewed layers. + +use std::fmt; + +const NATIVE_MESSAGING_HOST_TO_CHROME_MAX_BYTES: usize = 1_048_576; +const NATIVE_MESSAGING_CHROME_TO_HOST_MAX_BYTES: usize = 67_108_864; +const NATIVE_MESSAGING_LENGTH_BYTES: usize = 4; + +/// Direction of one Chrome native-messaging payload. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeMessagingFrameDirection { + /// A message written by Chrome and read by the native host. + ChromeToHost, + /// A message written by the native host and read by Chrome. + HostToChrome, +} + +impl NativeMessagingFrameDirection { + const fn maximum_payload_bytes(self) -> usize { + match self { + Self::ChromeToHost => NATIVE_MESSAGING_CHROME_TO_HOST_MAX_BYTES, + Self::HostToChrome => NATIVE_MESSAGING_HOST_TO_CHROME_MAX_BYTES, + } + } +} + +/// A fail-closed native-messaging frame validation failure. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeMessagingFrameError { + /// Fewer than four bytes were available for the native-endian length header. + TruncatedHeader, + /// The declared or outbound payload exceeds Chrome's limit for this direction. + PayloadTooLarge { + /// Number of payload bytes declared or supplied. + declared_bytes: usize, + /// Maximum payload bytes Chrome permits for this direction. + maximum_bytes: usize, + }, + /// The frame body length does not exactly match the declared byte length. + LengthMismatch { + /// Number of payload bytes declared by the frame header. + declared_bytes: usize, + /// Number of payload bytes actually present after the header. + actual_bytes: usize, + }, + /// The frame body is not valid UTF-8 and therefore cannot be a JSON text message. + InvalidUtf8, +} + +impl fmt::Display for NativeMessagingFrameError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::TruncatedHeader => { + formatter.write_str("native-messaging frame is missing its four-byte header") + } + Self::PayloadTooLarge { .. } => { + formatter.write_str("native-messaging payload exceeds the direction limit") + } + Self::LengthMismatch { .. } => { + formatter.write_str("native-messaging frame length does not match its header") + } + Self::InvalidUtf8 => { + formatter.write_str("native-messaging payload is not valid UTF-8") + } + } + } +} + +impl std::error::Error for NativeMessagingFrameError {} + +/// Encode one already-serialized UTF-8 JSON message as a bounded Chrome native frame. +/// +/// This function does not validate JSON syntax. Callers must serialize a reviewed +/// message schema before this framing step and must keep authority decisions outside +/// the payload codec. +pub fn encode_native_messaging_frame( + payload: &str, + direction: NativeMessagingFrameDirection, +) -> Result, NativeMessagingFrameError> { + let payload_bytes = payload.as_bytes(); + let maximum_bytes = direction.maximum_payload_bytes(); + if payload_bytes.len() > maximum_bytes { + return Err(NativeMessagingFrameError::PayloadTooLarge { + declared_bytes: payload_bytes.len(), + maximum_bytes, + }); + } + + let declared_length = u32::try_from(payload_bytes.len()).map_err(|_| { + NativeMessagingFrameError::PayloadTooLarge { + declared_bytes: payload_bytes.len(), + maximum_bytes, + } + })?; + let mut frame = Vec::with_capacity(NATIVE_MESSAGING_LENGTH_BYTES + payload_bytes.len()); + frame.extend_from_slice(&declared_length.to_ne_bytes()); + frame.extend_from_slice(payload_bytes); + Ok(frame) +} + +/// Decode one complete bounded Chrome native-messaging frame as UTF-8 text. +/// +/// Declared size is checked before the body is interpreted, and the complete input +/// must contain exactly one frame. The returned text borrows the caller-owned frame. +pub fn decode_native_messaging_frame( + frame: &[u8], + direction: NativeMessagingFrameDirection, +) -> Result<&str, NativeMessagingFrameError> { + if frame.len() < NATIVE_MESSAGING_LENGTH_BYTES { + return Err(NativeMessagingFrameError::TruncatedHeader); + } + + let header = [frame[0], frame[1], frame[2], frame[3]]; + let declared_bytes = u32::from_ne_bytes(header) as usize; + let maximum_bytes = direction.maximum_payload_bytes(); + if declared_bytes > maximum_bytes { + return Err(NativeMessagingFrameError::PayloadTooLarge { + declared_bytes, + maximum_bytes, + }); + } + + let payload = &frame[NATIVE_MESSAGING_LENGTH_BYTES..]; + if payload.len() != declared_bytes { + return Err(NativeMessagingFrameError::LengthMismatch { + declared_bytes, + actual_bytes: payload.len(), + }); + } + + std::str::from_utf8(payload).map_err(|_| NativeMessagingFrameError::InvalidUtf8) +} From 90c44c03df7d040df90fb853e63ade0976fb85ef Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:04:20 -0700 Subject: [PATCH 4/9] feat(extension): export native messaging frame codec --- crates/originweave-core/src/crate_root.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/crates/originweave-core/src/crate_root.rs b/crates/originweave-core/src/crate_root.rs index 9aaca7003..d09b0f3f3 100644 --- a/crates/originweave-core/src/crate_root.rs +++ b/crates/originweave-core/src/crate_root.rs @@ -7,7 +7,9 @@ use std::fmt; #[path = "lib.rs"] mod base; +mod native_messaging_frame; pub use base::*; +pub use native_messaging_frame::*; impl fmt::Display for NativeMessagingHostNameError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { From 2deb1947da9194b3860c52e6bc14f943e230be4f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:04:55 -0700 Subject: [PATCH 5/9] refactor(extension): remove unreachable frame length conversion branch --- crates/originweave-core/src/native_messaging_frame.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_frame.rs b/crates/originweave-core/src/native_messaging_frame.rs index 96f7e4ce2..c551abf2b 100644 --- a/crates/originweave-core/src/native_messaging_frame.rs +++ b/crates/originweave-core/src/native_messaging_frame.rs @@ -91,12 +91,9 @@ pub fn encode_native_messaging_frame( }); } - let declared_length = u32::try_from(payload_bytes.len()).map_err(|_| { - NativeMessagingFrameError::PayloadTooLarge { - declared_bytes: payload_bytes.len(), - maximum_bytes, - } - })?; + // Both reviewed Chrome direction limits are far below u32::MAX, so the + // preceding bound proves this conversion cannot truncate. + let declared_length = payload_bytes.len() as u32; let mut frame = Vec::with_capacity(NATIVE_MESSAGING_LENGTH_BYTES + payload_bytes.len()); frame.extend_from_slice(&declared_length.to_ne_bytes()); frame.extend_from_slice(payload_bytes); From 6903681b26ac597db0973b4c610527e07c227b07 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:05:17 -0700 Subject: [PATCH 6/9] test(extension): cover native messaging frame diagnostics --- .../tests/native_messaging_frame_codec.rs | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/crates/originweave-core/tests/native_messaging_frame_codec.rs b/crates/originweave-core/tests/native_messaging_frame_codec.rs index 9a7341841..3bafccd32 100644 --- a/crates/originweave-core/tests/native_messaging_frame_codec.rs +++ b/crates/originweave-core/tests/native_messaging_frame_codec.rs @@ -105,6 +105,38 @@ fn native_messaging_encode_enforces_host_to_chrome_limit() { ); } +#[test] +fn native_messaging_errors_are_deterministic_and_payload_free() { + let errors = [ + ( + NativeMessagingFrameError::TruncatedHeader, + "native-messaging frame is missing its four-byte header", + ), + ( + NativeMessagingFrameError::PayloadTooLarge { + declared_bytes: 2, + maximum_bytes: 1, + }, + "native-messaging payload exceeds the direction limit", + ), + ( + NativeMessagingFrameError::LengthMismatch { + declared_bytes: 2, + actual_bytes: 1, + }, + "native-messaging frame length does not match its header", + ), + ( + NativeMessagingFrameError::InvalidUtf8, + "native-messaging payload is not valid UTF-8", + ), + ]; + + for (error, expected) in errors { + assert_eq!(error.to_string(), expected); + } +} + #[test] fn native_messaging_decode_rejects_non_utf8_payload_without_reflecting_bytes() { let frame = frame_with_declared_length(2, &[0xff, 0xfe]); From dfdb0fb8ed52311e4991c643a7a18f5145affb79 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:06:34 -0700 Subject: [PATCH 7/9] style(extension): apply canonical rustfmt to frame codec --- crates/originweave-core/src/native_messaging_frame.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_frame.rs b/crates/originweave-core/src/native_messaging_frame.rs index c551abf2b..8e38c5aae 100644 --- a/crates/originweave-core/src/native_messaging_frame.rs +++ b/crates/originweave-core/src/native_messaging_frame.rs @@ -64,9 +64,7 @@ impl fmt::Display for NativeMessagingFrameError { Self::LengthMismatch { .. } => { formatter.write_str("native-messaging frame length does not match its header") } - Self::InvalidUtf8 => { - formatter.write_str("native-messaging payload is not valid UTF-8") - } + Self::InvalidUtf8 => formatter.write_str("native-messaging payload is not valid UTF-8"), } } } From 8f8726603d3f385653bc483a56daed681b303ecf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:08:50 -0700 Subject: [PATCH 8/9] docs(research): record native messaging framing contract --- docs/doctoring/browser-agent-protocols.md | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/docs/doctoring/browser-agent-protocols.md b/docs/doctoring/browser-agent-protocols.md index 5173a32e6..4b2342962 100644 --- a/docs/doctoring/browser-agent-protocols.md +++ b/docs/doctoring/browser-agent-protocols.md @@ -1,10 +1,10 @@ # Browser and Agent Protocol Standards Evidence -- **Reviewed:** 2026-08-10 +- **Reviewed:** 2026-08-21 - **Purpose:** primary-source evidence for OriginWeave browser compatibility and adapter boundaries - **Canonical research index:** [`../doctoring.md`](../doctoring.md) -This addendum complements the main doctoring record. The main record already carries the WebDriver BiDi, WARC/ISO 28500 and W3C PROV-O evidence. This addendum records the current primary sources for Manifest V3, Chrome DevTools Protocol, WebMCP and Model Context Protocol so product documentation does not rely on uncited protocol names. +This addendum complements the main doctoring record. The main record already carries the WebDriver BiDi, WARC/ISO 28500 and W3C PROV-O evidence. This addendum records the current primary sources for Manifest V3, Chrome native messaging, Chrome DevTools Protocol, WebMCP and Model Context Protocol so product documentation does not rely on uncited protocol names. ## WebDriver BiDi @@ -20,6 +20,14 @@ A Chrome extension permission remains separate from an OriginWeave Agent capabil Primary source: Chrome for Developers, *Manifest file format* and *Manifest Version*. +## Chrome native messaging + +Chrome's native-messaging contract launches each registered native host in a separate process and exchanges UTF-8 JSON messages over standard input and standard output. Each message is prefixed by a 32-bit unsigned length in native byte order. Chrome documents a 1 MiB maximum for a message sent from the native host to Chrome and a 64 MiB maximum for a message sent from Chrome to the native host. Chrome also passes the calling extension origin to the native host process, but that process argument is not itself OriginWeave authorization. + +OriginWeave therefore keeps native-messaging framing as a narrow byte-level boundary. Manifest and operating-system registration, process identity and supervision, caller-origin binding, JSON schema validation, extension-to-host authority, Agent capability, and secret disclosure remain separately reviewed fail-closed layers. + +Primary source: Chrome for Developers, *Native messaging*. + ## Chrome DevTools Protocol The official CDP documentation states that tip-of-tree changes frequently and provides no backward-compatibility guarantee for capabilities it introduces. OriginWeave therefore pins the Chromium/protocol evidence used by a release and keeps CDP behind an adapter. CDP is useful for Chromium-specific Network, Accessibility, DOMSnapshot, tracing and diagnostic surfaces; it is not the durable OriginWeave authority model. @@ -49,10 +57,11 @@ The main [`docs/doctoring.md`](../doctoring.md) records the stable W3C PROV-O Re 1. Version adapter contracts independently from OriginWeave session/context/action/evidence types. 2. Pin exact Chromium/CDP compatibility evidence at release time. 3. Keep WebDriver BiDi's Working Draft status visible in compatibility claims. -4. Keep WebMCP experimental/optional and propagate untrusted-content semantics. -5. Keep MCP browser state application-level rather than equating protocol transport/session metadata with browser authority. -6. Test Manifest V3 compatibility and extension-to-Agent authority isolation as separate evidence classes. -7. Treat WARC/PROV as provenance representations, not policy or truth escalation. +4. Treat native-messaging framing as a bounded transport codec, never as host registration, process identity, Agent capability, or secret authority. +5. Keep WebMCP experimental/optional and propagate untrusted-content semantics. +6. Keep MCP browser state application-level rather than equating protocol transport/session metadata with browser authority. +7. Test Manifest V3 compatibility and extension-to-Agent authority isolation as separate evidence classes. +8. Treat WARC/PROV as provenance representations, not policy or truth escalation. ## References — APA 7th @@ -62,6 +71,8 @@ Google Chrome Developers. (n.d.). *Manifest file format*. Chrome for Developers. Google Chrome Developers. (n.d.). *Manifest Version*. Chrome for Developers. Retrieved August 10, 2026, from https://developer.chrome.com/docs/extensions/reference/manifest/manifest-version +Google Chrome Developers. (n.d.). *Native messaging*. Chrome for Developers. Retrieved August 21, 2026, from https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging + Google Chrome Developers. (2026). *WebMCP*. Chrome for Developers. https://developer.chrome.com/docs/ai/webmcp Pagnucco, J., & Klepper, A. (2026, June 9). *Agent security considerations for WebMCP*. Chrome for Developers. https://developer.chrome.com/docs/agents/security From 57dc0b4886cff1dc22a2ab1c251c5e5bff84158e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 21 Aug 2026 01:09:27 -0700 Subject: [PATCH 9/9] docs(changelog): record native messaging frame boundary --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d17419927..54a1d9b26 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Added +- Bound Chrome native-messaging byte framing with native-endian 32-bit lengths, exact one-frame matching, direction-specific 1 MiB host-to-Chrome and 64 MiB Chrome-to-host limits, and UTF-8 fail-closed validation without granting process, JSON, Agent, or secret authority. - 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. - Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules.