Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
73 commits
Select commit Hold shift + click to select a range
f8b0539
test(extension): define native host manifest authority contract
seonghobae Aug 16, 2026
eca0848
feat(extension): validate native host manifest authority
seonghobae Aug 16, 2026
afa8501
refactor(core): expose native manifest authority module
seonghobae Aug 16, 2026
e5d7784
refactor(core): route crate root through modular contracts
seonghobae Aug 16, 2026
a2c1bcc
docs(changelog): record native host manifest authority
seonghobae Aug 16, 2026
6d85f1a
docs(doctoring): record native messaging manifest authority
seonghobae Aug 16, 2026
c78ff47
docs(traceability): map native messaging authority stack
seonghobae Aug 16, 2026
ea89ba0
test(extension): bind native host executable path
seonghobae Aug 16, 2026
d1423e7
test(extension): apply canonical rustfmt
seonghobae Aug 16, 2026
cc6c6b1
feat(extension): bind native host executable path
seonghobae Aug 16, 2026
0f781c4
feat(extension): expose native host platform authority
seonghobae Aug 16, 2026
fdd674b
test(extension): bound native host executable path
seonghobae Aug 16, 2026
b6d2601
feat(extension): bound native host executable path
seonghobae Aug 16, 2026
03f3931
Merge current native messaging framing into manifest authority
seonghobae Aug 17, 2026
2133ded
Merge current native messaging framing into manifest authority
seonghobae Aug 17, 2026
56a3a09
test(extension): bound native manifest document ingress
seonghobae Aug 17, 2026
5d10c28
feat(extension): bound native manifest document ingress
seonghobae Aug 17, 2026
6a3bf02
feat(extension): expose bounded manifest document ingress
seonghobae Aug 17, 2026
d15a001
style(extension): apply canonical manifest ingress formatting
seonghobae Aug 17, 2026
6218c54
docs(extension): record bounded manifest document ingress
seonghobae Aug 17, 2026
37d0d30
docs(extension): trace bounded manifest ingress authority
seonghobae Aug 17, 2026
8e61166
test(extension): require native-initiated manifest declaration
seonghobae Aug 17, 2026
45136e4
feat(extension): retain native-initiated manifest declaration
seonghobae Aug 17, 2026
14f4452
docs(changelog): record native-initiated manifest declaration
seonghobae Aug 17, 2026
db14d43
docs(extension): trace native-initiated declaration boundary
seonghobae Aug 17, 2026
3bb3d98
docs(research): pin native-initiated manifest evidence
seonghobae Aug 17, 2026
8000615
test(extension): require object-shaped native host manifest
seonghobae Aug 18, 2026
4c2801c
fix(extension): bound native host manifest object envelope
seonghobae Aug 18, 2026
cbf5089
docs(extension): trace native manifest envelope guard
seonghobae Aug 18, 2026
62a3328
test(core): require complete native manifest parsing
seonghobae Aug 18, 2026
67701d2
test(core): format native manifest parser regression
seonghobae Aug 18, 2026
212b073
feat(core): parse complete native host manifest documents
seonghobae Aug 18, 2026
b8c239d
feat(core): export native manifest parser errors
seonghobae Aug 18, 2026
a99a0d0
style(core): format native manifest parser
seonghobae Aug 18, 2026
b4d583e
test(core): cover native manifest parser failure edges
seonghobae Aug 18, 2026
72c0014
test(extension): close manifest parser coverage gaps
seonghobae Aug 18, 2026
8c00051
style(extension): apply canonical manifest edge formatting
seonghobae Aug 18, 2026
75b4da3
test(extension): close manifest parser coverage gaps
seonghobae Aug 18, 2026
075612d
test(extension): close parser CI gaps
seonghobae Aug 18, 2026
934f16e
test(extension): cover malformed manifest separator
seonghobae Aug 19, 2026
1e0d724
docs(extension): align manifest parser traceability
seonghobae Aug 19, 2026
3d8821b
test(extension): exercise public parser rejection edges
seonghobae Aug 19, 2026
3878d6b
test(extension): close native manifest parser coverage edges
seonghobae Aug 19, 2026
419e88b
style(extension): apply canonical rustfmt
seonghobae Aug 19, 2026
e9da4dd
test(extension): cover malformed manifest member key
seonghobae Aug 19, 2026
d28e72a
refactor(extension): encode manifest envelope invariant
seonghobae Aug 19, 2026
df71b52
fix(core): avoid panic-only manifest test helper
seonghobae Aug 19, 2026
ba269ff
test(core): cover manifest helper pre-parser failure
seonghobae Aug 19, 2026
6631e6e
test(extension): bound manifest origins before excess decode
seonghobae Aug 20, 2026
d9b094a
fix(extension): enforce manifest origin budget during parsing
seonghobae Aug 20, 2026
c2d4668
chore(extension): apply canonical rustfmt ordering
seonghobae Aug 20, 2026
afaa4b0
fix(extension): preserve parser contracts while formatting
seonghobae Aug 20, 2026
33b8329
test(native-messaging): reject empty host descriptions
seonghobae Aug 20, 2026
1d384f3
style(native-messaging): format description regression
seonghobae Aug 20, 2026
a72967e
test(native-messaging): cover invalid description error
seonghobae Aug 20, 2026
e9ed78b
fix(native-messaging): reject empty host descriptions
seonghobae Aug 20, 2026
7cddb51
style(native-messaging): format description error coverage
seonghobae Aug 20, 2026
9a84b16
test(native-messaging): keep description regression lint-clean
seonghobae Aug 20, 2026
93e5b4c
merge(parent): reconcile native host manifest after #154
seonghobae Aug 24, 2026
d66bbbe
merge(parent): realign native host manifest with current framing parent
seonghobae Aug 24, 2026
a0fb625
fix(stack): preserve current native messaging framing truth
seonghobae Aug 24, 2026
1cd62ee
docs: preserve parent changelog and record native manifest authority
seonghobae Aug 24, 2026
10dec81
merge(parent): realign native manifest with current framing parent
seonghobae Aug 24, 2026
b86d094
docs(stack): preserve current framing changelog truth
seonghobae Aug 24, 2026
1ef60d3
chore(stack): merge current native-messaging framing prerequisite
seonghobae Aug 25, 2026
f19b79f
chore(stack): merge current native-messaging framing prerequisite
seonghobae Aug 25, 2026
e498dee
merge(stack): reconcile native host manifest with live framing parent
seonghobae Aug 28, 2026
538ca90
test(native-messaging): bound descendant stream read requests
seonghobae Aug 29, 2026
545aa47
fix(native-messaging): preserve bounded descendant stream reads
seonghobae Aug 29, 2026
2a65d27
chore(stack): reconstruct native messaging manifest on current framin…
seonghobae Aug 29, 2026
bdfb579
test(ddd): require native manifest extension ownership
seonghobae Sep 1, 2026
667d262
refactor(extension): own native host manifest policy
seonghobae Sep 2, 2026
0da81d9
fix(extension): restore workspace verification
seonghobae Sep 4, 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
7 changes: 7 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
[workspace]
members = [
"crates/originweave-core",
"crates/originweave-extension",
"crates/originweave-bap",
"crates/originweave-policy",
"crates/originweave-resource",
Expand Down
1 change: 1 addition & 0 deletions crates/originweave-core/src/root.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,6 @@ pub use contracts::*;
pub mod mcp;
mod native_messaging;
pub use native_messaging::*;

/// Deterministic fail-closed release benchmark acceptance aggregation.
pub mod release_acceptance;
20 changes: 20 additions & 0 deletions crates/originweave-extension/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
[package]
name = "originweave-extension"
description = "OriginWeave extension and native-host policy contracts."
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
authors.workspace = true
repository.workspace = true
homepage.workspace = true
publish = false

[lib]
path = "src/root.rs"

[dependencies]
originweave-core = { path = "../originweave-core" }

[lints]
workspace = true
271 changes: 271 additions & 0 deletions crates/originweave-extension/src/native_messaging_manifest.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,271 @@
//! Deterministic authority extracted from one validated Chrome native-messaging host manifest.
//!
//! This module validates caller-supplied manifest fields only. It does not prove that a
//! manifest is installed, that an executable path is owned by a trusted principal, or that
//! any process attached to stdio is the host named by the manifest. Runtime adapters must
//! establish those boundaries independently before composing this evidence with process
//! authority.

use std::collections::BTreeSet;
use std::fmt;

use crate::{ExtensionId, NativeMessagingAccessRequest, NativeMessagingHostName};

/// Maximum number of raw `allowed_origins` entries accepted from one host manifest.
///
/// Chrome does not define this OriginWeave-specific safety budget. The limit bounds work
/// before duplicate origins are collapsed and therefore prevents a syntactically valid
/// manifest from turning policy admission into unbounded allocation or comparison work.
pub const MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS: usize = 256;

/// Maximum UTF-8 byte length accepted for one declared native-host executable path.
///
/// This 32 KiB value is an OriginWeave allocation safety budget, not a Chrome or operating-
/// system path-validity limit. Runtime adapters remain responsible for platform-native path
/// resolution, canonicalization, ownership, and executable identity checks.
pub const MAX_NATIVE_MESSAGING_EXECUTABLE_PATH_BYTES: usize = 32 * 1024;

/// Operating-system path semantics used by one native-messaging host manifest.
///
/// Chrome requires absolute native-host paths on Linux and macOS, while Windows also allows
/// paths relative to the manifest directory. OriginWeave records the platform explicitly so
/// later runtime adapters cannot reinterpret a validated path under different semantics.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeMessagingHostPlatform {
/// Linux native-messaging host-manifest semantics.
Linux,
/// macOS native-messaging host-manifest semantics.
MacOs,
/// Windows native-messaging host-manifest semantics.
Windows,
}

/// Validated authority-bearing fields from one Chrome native-messaging host manifest.
///
/// The record contains the exact host identity, declared executable-path text and platform,
/// exact Chromium extension identities named by the manifest's `allowed_origins`, and whether
/// the manifest explicitly declares support for native-initiated connections. Possessing this
/// value is not proof of manifest installation, path canonicalization, executable existence or
/// ownership, process identity, message provenance, Chrome feature/policy enablement, or Agent
/// authority.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct NativeMessagingHostManifest {
host_name: NativeMessagingHostName,
platform: NativeMessagingHostPlatform,
executable_path: String,
allowed_extensions: BTreeSet<ExtensionId>,
supports_native_initiated_connections: bool,
}

impl NativeMessagingHostManifest {
/// Validate the authority-bearing host-manifest fields without widening them.
///
/// This compatibility constructor records no native-initiated-connection declaration.
/// Call [`Self::parse_with_native_initiated_connections`] only when a trusted structured
/// parser has explicitly validated that optional manifest field.
pub fn parse(
host_name: NativeMessagingHostName,
platform: NativeMessagingHostPlatform,
executable_path: &str,
interface_type: &str,
allowed_origins: &[&str],
) -> Result<Self, NativeMessagingHostManifestError> {
Self::parse_with_native_initiated_connections(
host_name,
platform,
executable_path,
interface_type,
false,
allowed_origins,
)
}

/// Validate authority-bearing host-manifest fields plus the optional native-initiation flag.
///
/// `interface_type` must be exactly `stdio`. Linux and macOS executable paths must be
/// absolute, matching Chrome's native-messaging contract; Windows relative paths remain
/// relative and must be resolved by a trusted runtime adapter against the authenticated
/// manifest directory. Empty paths, embedded NUL bytes, and paths exceeding the
/// OriginWeave allocation budget are rejected before storage on every platform. Every
/// allowed origin must be exactly `chrome-extension://<canonical-extension-id>/`;
/// alternate schemes, wildcards, suffix paths, query strings, fragments, and
/// non-canonical extension identities are rejected rather than normalized. The raw list
/// is bounded before deduplication.
///
/// `supports_native_initiated_connections` records only the validated manifest declaration.
/// It does not prove that Chromium enables the corresponding feature, that policy permits
/// it, that a process is the declared host, or that any native-initiated request has Agent
/// authority.
pub fn parse_with_native_initiated_connections(
host_name: NativeMessagingHostName,
platform: NativeMessagingHostPlatform,
executable_path: &str,
interface_type: &str,
supports_native_initiated_connections: bool,
allowed_origins: &[&str],
) -> Result<Self, NativeMessagingHostManifestError> {
if interface_type != "stdio" {
return Err(NativeMessagingHostManifestError::UnsupportedInterfaceType);
}
validate_executable_path(platform, executable_path)?;
if allowed_origins.is_empty() {
return Err(NativeMessagingHostManifestError::MissingAllowedOrigin);
}
if allowed_origins.len() > MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS {
return Err(NativeMessagingHostManifestError::TooManyAllowedOrigins);
}

let mut allowed_extensions = BTreeSet::new();
for origin in allowed_origins {
allowed_extensions.insert(parse_extension_origin(origin)?);
}

Ok(Self {
host_name,
platform,
executable_path: executable_path.to_owned(),
allowed_extensions,
supports_native_initiated_connections,
})
}

/// Return the exact native-messaging host identity declared by the manifest.
#[must_use]
pub const fn host_name(&self) -> &NativeMessagingHostName {
&self.host_name
}

/// Return the platform whose path semantics were used to validate the manifest.
#[must_use]
pub const fn platform(&self) -> NativeMessagingHostPlatform {
self.platform
}

/// Return the exact executable-path text declared by the manifest.
///
/// Windows relative paths are intentionally not resolved here because safe resolution
/// requires the authenticated manifest location. The returned path therefore carries no
/// filesystem-existence, canonicalization, ownership, or process-identity claim.
#[must_use]
pub fn executable_path(&self) -> &str {
&self.executable_path
}

/// Return the number of distinct exact extension identities explicitly allowed.
#[must_use]
pub fn allowed_extension_count(&self) -> usize {
self.allowed_extensions.len()
}

/// Return whether the validated manifest explicitly declares native-initiated connections.
///
/// A `true` value is declaration evidence only. It does not prove Chromium feature or
/// enterprise-policy enablement and grants no connection, process, message, or Agent
/// authority by itself.
#[must_use]
pub const fn supports_native_initiated_connections(&self) -> bool {
self.supports_native_initiated_connections
}

/// Evaluate one native-messaging request against this exact manifest authority.
///
/// Host identity is checked before extension membership. An `Allow` result means only
/// that the already-validated manifest fields name the exact request; it does not mint
/// Agent authority or attest the installed host process.
#[must_use]
pub fn evaluate(
&self,
request: &NativeMessagingAccessRequest,
) -> NativeMessagingHostManifestAccessDecision {
if request.host_name() != &self.host_name {
return NativeMessagingHostManifestAccessDecision::DenyHostMismatch;
}
if !self.allowed_extensions.contains(request.extension_id()) {
return NativeMessagingHostManifestAccessDecision::DenyExtensionNotAllowed;
}
NativeMessagingHostManifestAccessDecision::Allow
}
}

fn validate_executable_path(
platform: NativeMessagingHostPlatform,
executable_path: &str,
) -> Result<(), NativeMessagingHostManifestError> {
if executable_path.is_empty() || executable_path.contains('\0') {
return Err(NativeMessagingHostManifestError::InvalidExecutablePath);
}
if executable_path.len() > MAX_NATIVE_MESSAGING_EXECUTABLE_PATH_BYTES {
return Err(NativeMessagingHostManifestError::ExecutablePathTooLong);
}
if platform != NativeMessagingHostPlatform::Windows && !executable_path.starts_with('/') {
return Err(NativeMessagingHostManifestError::RelativeExecutablePathUnsupported);
}
Ok(())
}

fn parse_extension_origin(origin: &str) -> Result<ExtensionId, NativeMessagingHostManifestError> {
let Some(extension_text) = origin.strip_prefix("chrome-extension://") else {
return Err(NativeMessagingHostManifestError::InvalidAllowedOrigin);
};
let Some(extension_text) = extension_text.strip_suffix('/') else {
return Err(NativeMessagingHostManifestError::InvalidAllowedOrigin);
};
ExtensionId::parse(extension_text)
.map_err(|_error| NativeMessagingHostManifestError::InvalidAllowedOrigin)
}

/// Result of matching one native-messaging request to validated host-manifest authority.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeMessagingHostManifestAccessDecision {
/// The manifest names the exact requested host and explicitly allows the extension.
Allow,
/// The request names a different host from the validated manifest.
DenyHostMismatch,
/// The exact requesting extension is absent from the manifest allow-list.
DenyExtensionNotAllowed,
}

/// Failure to validate authority-bearing fields from a native-messaging host manifest.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum NativeMessagingHostManifestError {
/// The manifest interface type was not exactly Chrome's `stdio` value.
UnsupportedInterfaceType,
/// The manifest executable-path text was empty or contained an embedded NUL byte.
InvalidExecutablePath,
/// The manifest executable-path text exceeded the OriginWeave allocation safety budget.
ExecutablePathTooLong,
/// A non-Windows manifest used a relative executable path.
RelativeExecutablePathUnsupported,
/// The manifest did not explicitly allow any extension origin.
MissingAllowedOrigin,
/// The raw allowed-origin list exceeded the OriginWeave admission safety budget.
TooManyAllowedOrigins,
/// An allowed origin was not one exact canonical Chromium extension origin.
InvalidAllowedOrigin,
}

impl fmt::Display for NativeMessagingHostManifestError {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::UnsupportedInterfaceType => formatter
.write_str("native messaging host manifest interface type must be stdio"),
Self::InvalidExecutablePath => formatter
.write_str("native messaging host manifest contains an invalid executable path"),
Self::ExecutablePathTooLong => formatter.write_str(
"native messaging host manifest executable path exceeds the OriginWeave safety budget",
),
Self::RelativeExecutablePathUnsupported => formatter
.write_str("native messaging host executable path must be absolute on this platform"),
Self::MissingAllowedOrigin => formatter.write_str(
"native messaging host manifest must allow at least one exact extension origin",
),
Self::TooManyAllowedOrigins => formatter.write_str(
"native messaging host manifest exceeds the OriginWeave allowed-origin safety budget",
),
Self::InvalidAllowedOrigin => formatter
.write_str("native messaging host manifest contains an invalid extension origin"),
}
}
}

impl std::error::Error for NativeMessagingHostManifestError {}
Loading
Loading