Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
0918581
test(extension): define native messaging framing boundary
seonghobae Aug 14, 2026
b65373b
test(extension): canonicalize framing RED
seonghobae Aug 14, 2026
cdf290e
test(extension): apply canonical rustfmt to framing RED
seonghobae Aug 14, 2026
b5ec0ae
feat(extension): implement bounded native messaging framing
seonghobae Aug 14, 2026
9e0bdb0
style(extension): apply canonical rustfmt to framing boundary
seonghobae Aug 14, 2026
bd7066c
docs(extension): record native messaging framing boundary
seonghobae Aug 14, 2026
25242e8
docs(extension): doctor native messaging framing contract
seonghobae Aug 14, 2026
b7a17e2
test(extension): remove panic-based native framing assertions
seonghobae Aug 15, 2026
6999fac
test(extension): require UTF-8 native messaging payloads
seonghobae Aug 15, 2026
6103893
feat(extension): validate native messaging UTF-8
seonghobae Aug 15, 2026
9f39778
style(extension): apply canonical rustfmt
seonghobae Aug 15, 2026
9b5a333
docs(extension): record native messaging UTF-8 boundary
seonghobae Aug 15, 2026
9a84fac
docs(extension): record UTF-8 framing change
seonghobae Aug 15, 2026
5a66d36
test(extension): align native messaging identity contract
seonghobae Aug 15, 2026
d2e1ae8
Merge native messaging authority updates into framing boundary
seonghobae Aug 15, 2026
276e0d4
Merge current native messaging authority into framing boundary
seonghobae Aug 17, 2026
4a71b7d
Merge current native messaging authority into framing boundary
seonghobae Aug 17, 2026
7ad5f19
merge(parent): reconcile native messaging framing after #82
seonghobae Aug 24, 2026
93c9bb0
merge(parent): preserve native messaging authority changelog
seonghobae Aug 24, 2026
fcaa517
docs(changelog): correct extension origin binding text
seonghobae Aug 24, 2026
1f295ab
chore(stack): realign native-messaging framing on current authority p…
seonghobae Aug 24, 2026
fd5630a
docs(core): distinguish native messaging resource limits
seonghobae Aug 24, 2026
26b94fa
docs(mv3): correct native messaging size authority
seonghobae Aug 24, 2026
651690b
fix(stack): realign native framing with current host authority
seonghobae Aug 24, 2026
75280b9
test(extension): require bounded native messaging stream reads
seonghobae Aug 25, 2026
5099732
test(extension): normalize bounded stream regression formatting
seonghobae Aug 25, 2026
e8709a4
fix(extension): bound native messaging stream reads
seonghobae Aug 25, 2026
c1773cf
test(extension): cover native messaging stream read failures
seonghobae Aug 25, 2026
08642a8
style(extension): apply canonical stream-reader formatting
seonghobae Aug 25, 2026
0d0827d
test(extension): satisfy strict native messaging error assertions
seonghobae Aug 25, 2026
30c89f9
fix(extension): make stream coverage deterministic
seonghobae Aug 25, 2026
2454cec
merge(stack): reconcile native messaging framing with live parent
seonghobae Aug 27, 2026
77ef39b
docs(mv3): pin native messaging evidence
seonghobae Aug 28, 2026
c02f3fd
fix: bound native messaging stream read buffers
seonghobae Aug 28, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ All notable changes to OriginWeave are documented in this file. The format follo
- Bound explicit extension-to-Agent grants to exclusive trusted-time expiry in addition to extension identity, session, browsing context, and canonical origin, so a same-origin grant cannot be reused at or after the deadline.
- Bound explicit extension-to-Agent grants to the exact canonical origin in addition to extension identity, session, and browsing context, so a same-session navigation or port change cannot reuse the grant.
- Added bounded native-messaging host-name identity and explicit exact extension-to-host grants so Chrome native-messaging permission or a manifest host name cannot implicitly become OriginWeave Agent authority.
- Bounded Chrome native-messaging framing with native-endian 32-bit byte lengths, direction-specific 1 MiB host-to-browser and 64 MiB browser-to-host payload ceilings, bounded 64 KiB stream reads that avoid allocating a declared maximum before bytes arrive, fail-closed rejection of oversized, truncated, or trailing frame data, and explicit UTF-8 payload validation before framed bytes can be treated as native-messaging text, without granting Agent authority or claiming JSON trust.
- Pinned the native-messaging compatibility evidence to an immutable Chromium revision and connected its framing, resource-ceiling, and non-authority decisions to the standards doctoring record.
- Rust workspace for independently reusable core, policy, destination, network, TLS, resource, and evidence modules.
- Canonical HTTPS and loopback-origin boundary with case-normalized schemes and hosts, default-port normalization, IPv4/IPv6 handling, browser-special numeric-host rejection, and explicit malformed-input errors.
- Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes.
Expand Down Expand Up @@ -103,4 +105,4 @@ All notable changes to OriginWeave are documented in this file. The format follo
- The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it.
- The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels.

[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD
[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD
172 changes: 172 additions & 0 deletions crates/originweave-core/src/native_messaging.rs
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
//! Explicit Chrome native-messaging host authority without ambient Agent authority.

use std::fmt;
use std::io::Read;

use crate::ExtensionId;

const MAX_NATIVE_MESSAGING_HOST_NAME_BYTES: usize = 256;
const HOST_TO_BROWSER_NATIVE_MESSAGING_LIMIT: usize = 1_048_576;
const BROWSER_TO_HOST_NATIVE_MESSAGING_LIMIT: usize = 67_108_864;
const NATIVE_MESSAGING_READ_CHUNK_BYTES: usize = 64 * 1024;

/// A canonical Chrome native-messaging host name admitted to OriginWeave policy.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
Expand Down Expand Up @@ -157,3 +161,171 @@ pub fn evaluate_native_messaging_access(
}
NativeMessagingAccessDecision::Allow
}

/// Direction of one Chrome native-messaging frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeMessagingFrameDirection {
/// A frame written by a native host for delivery to the browser.
HostToBrowser,
/// A frame written by the browser for delivery to a native host.
BrowserToHost,
}

/// Failure to encode or decode a bounded Chrome native-messaging frame.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeMessagingFrameError {
/// Fewer than four bytes were available for the native-endian length prefix.
MissingLengthPrefix,
/// The advertised or supplied payload exceeds the limit for its direction.
PayloadTooLarge,
/// The complete frame length differs from the advertised payload length.
LengthMismatch,
/// The framed payload is not valid UTF-8 text.
InvalidUtf8Payload,
}

impl fmt::Display for NativeMessagingFrameError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::MissingLengthPrefix => {
formatter.write_str("native messaging frame is missing its 32-bit length prefix")
}
Self::PayloadTooLarge => {
formatter.write_str("native messaging payload exceeds the direction-specific limit")
}
Self::LengthMismatch => {
formatter.write_str("native messaging frame length does not match its prefix")
}
Self::InvalidUtf8Payload => {
formatter.write_str("native messaging payload is not valid UTF-8")
}
}
}
}

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

/// Failure while reading one bounded native-messaging payload from a stream.
#[derive(Debug)]
pub enum NativeMessagingFrameReadError {
/// Framing policy rejected the advertised payload before payload allocation or I/O.
Frame(NativeMessagingFrameError),
/// The underlying stream failed while reading the prefix or the admitted payload.
Io(std::io::Error),
}

impl fmt::Display for NativeMessagingFrameReadError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Frame(error) => error.fmt(formatter),
Self::Io(error) => error.fmt(formatter),
}
}
}

impl std::error::Error for NativeMessagingFrameReadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Frame(error) => Some(error),
Self::Io(error) => Some(error),
}
}
}

/// Return OriginWeave's reviewed payload ceiling for one native-messaging direction.
#[must_use]
pub const fn native_messaging_payload_limit(direction: NativeMessagingFrameDirection) -> usize {
match direction {
NativeMessagingFrameDirection::HostToBrowser => HOST_TO_BROWSER_NATIVE_MESSAGING_LIMIT,
NativeMessagingFrameDirection::BrowserToHost => BROWSER_TO_HOST_NATIVE_MESSAGING_LIMIT,
}
}

/// Read one native-messaging payload from a stream after enforcing the direction-specific budget.
///
/// The four-byte native-endian length prefix is read first. An oversized advertised length is
/// rejected before allocating or reading payload bytes. Stream failures retain their original
/// `std::io::Error` as a causal source. The returned bytes are untrusted framing output only;
/// JSON parsing, provenance, process identity, secrets, and Agent authority remain separate
/// fail-closed boundaries.
pub fn read_native_messaging_payload(
direction: NativeMessagingFrameDirection,
reader: &mut dyn Read,
) -> Result<Vec<u8>, NativeMessagingFrameReadError> {
let mut prefix = [0_u8; 4];
reader
.read_exact(&mut prefix)
.map_err(NativeMessagingFrameReadError::Io)?;

let advertised_length = u32::from_ne_bytes(prefix) as usize;
if advertised_length > native_messaging_payload_limit(direction) {
return Err(NativeMessagingFrameReadError::Frame(
NativeMessagingFrameError::PayloadTooLarge,
));
}

let mut payload = Vec::with_capacity(advertised_length.min(NATIVE_MESSAGING_READ_CHUNK_BYTES));
while payload.len() < advertised_length {
let chunk_start = payload.len();
let chunk_length = (advertised_length - chunk_start).min(NATIVE_MESSAGING_READ_CHUNK_BYTES);
payload.resize(chunk_start + chunk_length, 0);
reader
.read_exact(&mut payload[chunk_start..])
.map_err(NativeMessagingFrameReadError::Io)?;
}
Ok(payload)
}
Comment thread
seonghobae marked this conversation as resolved.

/// Encode one complete native-messaging frame with a native-endian 32-bit length prefix.
///
/// The payload is rejected before allocation when it exceeds the direction-specific
/// OriginWeave limit. The returned bytes are framing only and carry no trust or Agent authority.
pub fn encode_native_messaging_frame(
direction: NativeMessagingFrameDirection,
payload: &[u8],
) -> Result<Vec<u8>, NativeMessagingFrameError> {
if payload.len() > native_messaging_payload_limit(direction) {
return Err(NativeMessagingFrameError::PayloadTooLarge);
}

let payload_length = payload.len() as u32;
let mut frame = Vec::with_capacity(payload.len() + 4);
frame.extend_from_slice(&payload_length.to_ne_bytes());
frame.extend_from_slice(payload);
Ok(frame)
}

/// Decode one complete bounded native-messaging frame without allocating its payload.
///
/// Oversized advertised lengths are rejected before payload slicing. The frame must
/// contain exactly the advertised payload bytes; truncation and trailing data fail closed.
pub fn decode_native_messaging_frame(
direction: NativeMessagingFrameDirection,
frame: &[u8],
) -> Result<&[u8], NativeMessagingFrameError> {
if frame.len() < 4 {
return Err(NativeMessagingFrameError::MissingLengthPrefix);
}

let advertised_length = u32::from_ne_bytes([frame[0], frame[1], frame[2], frame[3]]) as usize;
if advertised_length > native_messaging_payload_limit(direction) {
return Err(NativeMessagingFrameError::PayloadTooLarge);
}
if frame.len() != advertised_length + 4 {
return Err(NativeMessagingFrameError::LengthMismatch);
}

Ok(&frame[4..])
}

/// Decode one bounded native-messaging frame and validate its payload as UTF-8 text.
///
/// This validates only framing and UTF-8 encoding. JSON syntax, message provenance, and
/// any Agent authority remain separate fail-closed boundaries for a later adapter.
pub fn decode_native_messaging_text_frame(
direction: NativeMessagingFrameDirection,
frame: &[u8],
) -> Result<&str, NativeMessagingFrameError> {
let payload = decode_native_messaging_frame(direction, frame)?;
std::str::from_utf8(payload).map_err(|_error| NativeMessagingFrameError::InvalidUtf8Payload)
}
Loading
Loading