From f8b0539d270dd0eaa5259985ad7f2ee14e22d771 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:32:25 +0900 Subject: [PATCH 01/64] test(extension): define native host manifest authority contract --- .../native_messaging_manifest_authority.rs | 130 ++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_manifest_authority.rs diff --git a/crates/originweave-core/tests/native_messaging_manifest_authority.rs b/crates/originweave-core/tests/native_messaging_manifest_authority.rs new file mode 100644 index 000000000..ffcfdf8cb --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_manifest_authority.rs @@ -0,0 +1,130 @@ +#![allow(clippy::expect_used)] + +use std::error::Error; + +use originweave_core::{ + ExtensionId, MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, NativeMessagingAccessRequest, + NativeMessagingHostManifest, NativeMessagingHostManifestAccessDecision, + NativeMessagingHostManifestError, NativeMessagingHostName, +}; + +const ALLOWED_EXTENSION: &str = "abcdefghijklmnopabcdefghijklmnop"; +const OTHER_EXTENSION: &str = "bcdefghijklmnopabcdefghijklmnopa"; + +fn extension_id(value: &str) -> ExtensionId { + ExtensionId::parse(value).expect("valid extension id") +} + +fn host_name(value: &str) -> NativeMessagingHostName { + NativeMessagingHostName::parse(value).expect("valid native messaging host name") +} + +fn extension_origin(value: &str) -> String { + format!("chrome-extension://{value}/") +} + +#[test] +fn manifest_binds_stdio_host_to_exact_allowed_extension_origins() -> Result<(), Box> { + let host = host_name("com.contextualwisdom.originweave"); + let allowed_origin = extension_origin(ALLOWED_EXTENSION); + let manifest = NativeMessagingHostManifest::parse( + host.clone(), + "stdio", + &[allowed_origin.as_str(), allowed_origin.as_str()], + )?; + + assert_eq!(manifest.host_name(), &host); + assert_eq!(manifest.allowed_extension_count(), 1); + + let exact = NativeMessagingAccessRequest::new(extension_id(ALLOWED_EXTENSION), host.clone()); + assert_eq!( + manifest.evaluate(&exact), + NativeMessagingHostManifestAccessDecision::Allow + ); + + let wrong_host = NativeMessagingAccessRequest::new( + extension_id(ALLOWED_EXTENSION), + host_name("com.contextualwisdom.other_host"), + ); + assert_eq!( + manifest.evaluate(&wrong_host), + NativeMessagingHostManifestAccessDecision::DenyHostMismatch + ); + + let wrong_extension = + NativeMessagingAccessRequest::new(extension_id(OTHER_EXTENSION), host.clone()); + assert_eq!( + manifest.evaluate(&wrong_extension), + NativeMessagingHostManifestAccessDecision::DenyExtensionNotAllowed + ); + Ok(()) +} + +#[test] +fn manifest_rejects_non_stdio_empty_and_oversized_allowlists() { + let host = host_name("com.contextualwisdom.originweave"); + let allowed_origin = extension_origin(ALLOWED_EXTENSION); + + assert_eq!( + NativeMessagingHostManifest::parse(host.clone(), "pipe", &[allowed_origin.as_str()]), + Err(NativeMessagingHostManifestError::UnsupportedInterfaceType) + ); + assert_eq!( + NativeMessagingHostManifest::parse(host.clone(), "stdio", &[]), + Err(NativeMessagingHostManifestError::MissingAllowedOrigin) + ); + + let oversized = vec![allowed_origin.as_str(); MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS + 1]; + assert_eq!( + NativeMessagingHostManifest::parse(host, "stdio", &oversized), + Err(NativeMessagingHostManifestError::TooManyAllowedOrigins) + ); +} + +#[test] +fn manifest_rejects_ambiguous_or_wildcard_extension_origins() { + let host = host_name("com.contextualwisdom.originweave"); + let invalid_origins = [ + "chrome-extension://*/", + "https://abcdefghijklmnopabcdefghijklmnop/", + "chrome-extension://abcdefghijklmnopabcdefghijklmnop", + "chrome-extension://abcdefghijklmnopabcdefghijklmnop/path", + "chrome-extension://ABCDEFGHIJKLMNOPABCDEFGHIJKLMNOP/", + "chrome-extension://abcdefghijklmnopabcdefghijklmnop/?query=1", + ]; + + for invalid in invalid_origins { + assert_eq!( + NativeMessagingHostManifest::parse(host.clone(), "stdio", &[invalid]), + Err(NativeMessagingHostManifestError::InvalidAllowedOrigin), + "unexpected allowed origin: {invalid:?}" + ); + } +} + +#[test] +fn manifest_error_messages_are_deterministic_and_source_free() { + let cases = [ + ( + NativeMessagingHostManifestError::UnsupportedInterfaceType, + "native messaging host manifest interface type must be stdio", + ), + ( + NativeMessagingHostManifestError::MissingAllowedOrigin, + "native messaging host manifest must allow at least one exact extension origin", + ), + ( + NativeMessagingHostManifestError::TooManyAllowedOrigins, + "native messaging host manifest exceeds the OriginWeave allowed-origin safety budget", + ), + ( + NativeMessagingHostManifestError::InvalidAllowedOrigin, + "native messaging host manifest contains an invalid extension origin", + ), + ]; + + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + assert!(error.source().is_none()); + } +} From eca0848deaf37f79f0eb183904278f73264de86d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:37:55 +0900 Subject: [PATCH 02/64] feat(extension): validate native host manifest authority --- .../src/native_messaging_manifest.rs | 150 ++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 crates/originweave-core/src/native_messaging_manifest.rs diff --git a/crates/originweave-core/src/native_messaging_manifest.rs b/crates/originweave-core/src/native_messaging_manifest.rs new file mode 100644 index 000000000..f44f8a97e --- /dev/null +++ b/crates/originweave-core/src/native_messaging_manifest.rs @@ -0,0 +1,150 @@ +//! 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; + +/// Validated authority-bearing fields from one Chrome native-messaging host manifest. +/// +/// The record contains only the exact host identity and exact Chromium extension identities +/// named by the manifest's `allowed_origins`. Possessing this value is not proof of manifest +/// installation, executable ownership, process identity, message provenance, or Agent +/// authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeMessagingHostManifest { + host_name: NativeMessagingHostName, + allowed_extensions: BTreeSet, +} + +impl NativeMessagingHostManifest { + /// Validate the authority-bearing host-manifest fields without widening them. + /// + /// `interface_type` must be exactly `stdio`. Every allowed origin must be exactly + /// `chrome-extension:///`; 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. + pub fn parse( + host_name: NativeMessagingHostName, + interface_type: &str, + allowed_origins: &[&str], + ) -> Result { + if interface_type != "stdio" { + return Err(NativeMessagingHostManifestError::UnsupportedInterfaceType); + } + 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, + allowed_extensions, + }) + } + + /// 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 number of distinct exact extension identities explicitly allowed. + #[must_use] + pub fn allowed_extension_count(&self) -> usize { + self.allowed_extensions.len() + } + + /// 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 parse_extension_origin(origin: &str) -> Result { + 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 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::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 {} From afa85015ac140d8f00d7efe86066a9adf60e8a21 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:38:21 +0900 Subject: [PATCH 03/64] refactor(core): expose native manifest authority module --- crates/originweave-core/src/root.rs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 crates/originweave-core/src/root.rs diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs new file mode 100644 index 000000000..a826263ec --- /dev/null +++ b/crates/originweave-core/src/root.rs @@ -0,0 +1,18 @@ +//! Shared security and governance contracts for OriginWeave. +//! +//! The crate keeps deterministic authority contracts independent from browser-engine +//! integration so the browser shell, headless runtime, MCP adapter, and enterprise +//! policy service can compose them without ambient authority inheritance. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +#[path = "lib.rs"] +mod legacy_contracts; +pub use legacy_contracts::*; + +mod native_messaging_manifest; +pub use native_messaging_manifest::{ + MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, NativeMessagingHostManifest, + NativeMessagingHostManifestAccessDecision, NativeMessagingHostManifestError, +}; From e5d7784755503f0fa7a5213782f91713021cac5a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:39:00 +0900 Subject: [PATCH 04/64] refactor(core): route crate root through modular contracts --- crates/originweave-core/Cargo.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/originweave-core/Cargo.toml b/crates/originweave-core/Cargo.toml index 35c83b19b..517e41217 100644 --- a/crates/originweave-core/Cargo.toml +++ b/crates/originweave-core/Cargo.toml @@ -10,6 +10,9 @@ repository.workspace = true homepage.workspace = true publish = false +[lib] +path = "src/root.rs" + [dependencies] [lints] From a2c1bcc0e195a83a7a505f235053aa9c7ca62a97 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:42:29 +0900 Subject: [PATCH 05/64] docs(changelog): record native host manifest authority --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e9eecc2b..0e713ede8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. +- Fail-closed native-messaging host-manifest authority that accepts only exact `stdio`, bounds raw `allowed_origins` before deduplication, validates only exact `chrome-extension:///` entries, and matches exact host/extension identity without treating manifest installation or process state as 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. From 6d85f1a15e1501f48ce2d12d560323a36b38719b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:44:31 +0900 Subject: [PATCH 06/64] docs(doctoring): record native messaging manifest authority --- docs/doctoring.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/docs/doctoring.md b/docs/doctoring.md index 75c107ef0..235be0d0a 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -8,6 +8,10 @@ This document records external evidence that changes OriginWeave architecture, t The 1 June 2026 WebDriver BiDi Working Draft defines a bidirectional remote-control protocol, events, commands, and user contexts. Because it remains a W3C Working Draft, OriginWeave places BiDi behind a versioned adapter and Web Platform Tests-derived contract tests rather than make it the internal authority model. +### Native-messaging manifest authority + +Chrome's current native-messaging documentation defines the host manifest as a separate configuration boundary containing an exact host `name`, executable `path`, interface `type`, and extension `allowed_origins`. The only documented interface type is `stdio`, and `allowed_origins` does not permit wildcard extension origins. Chrome starts the native host as a separate process and communicates through standard input and standard output. OriginWeave therefore treats validated host-manifest identity as one explicit authority input rather than as proof of installation, executable ownership, process identity, message provenance, or Agent authority. The first manifest contract accepts only exact `stdio`, exact canonical `chrome-extension:///` origins, and a bounded raw allow-list before deduplication; process registration, path ownership, spawning, sandboxing, and authenticated stdio remain separate boundaries. + ### 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`. @@ -116,6 +120,8 @@ Fielding, R., Nottingham, M., & Reschke, J. (2022). *HTTP semantics* (RFC 9110). Fugu Team, Sakana AI. (2026). *Sakana Fugu technical report* [Technical report]. arXiv. https://doi.org/10.48550/arXiv.2606.21228 +Google. (2023, February 27). *Native messaging*. Chrome for Developers. https://developer.chrome.com/docs/extensions/develop/concepts/native-messaging + Huston, G., & Buraglio, N. (2024). *Expanding the IPv6 documentation space* (RFC 9637). Internet Engineering Task Force. https://doi.org/10.17487/RFC9637 Internet Assigned Numbers Authority. (2025, October 9). *IPv4 special-purpose address space*. https://www.iana.org/assignments/iana-ipv4-special-registry/iana-ipv4-special-registry.xhtml @@ -158,4 +164,4 @@ World Wide Web Consortium. (2026, June 1). *WebDriver BiDi* (W3C Working Draft). Xu, J., Sun, Q., Schwendeman, P., Nielsen, S., Cetin, E., & Tang, Y. (2025). *TRINITY: An evolved LLM coordinator* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04695 -Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 +Zhou, S., Xu, F. F., Zhu, H., Zhou, X., Lo, R., Sridhar, A., Cheng, X., Ou, T., Bisk, Y., Fried, D., Alon, U., & Neubig, G. (2023). *WebArena: A realistic web environment for building autonomous agents*. arXiv. https://doi.org/10.48550/arXiv.2307.13854 \ No newline at end of file From c78ff47c65a535b6b635d9c1de7060d8287f72af Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:47:11 +0900 Subject: [PATCH 07/64] docs(traceability): map native messaging authority stack --- .../extension-authority-security.md | 55 +++++++++++++++---- 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/docs/traceability/extension-authority-security.md b/docs/traceability/extension-authority-security.md index a36380a31..a76d7fe9d 100644 --- a/docs/traceability/extension-authority-security.md +++ b/docs/traceability/extension-authority-security.md @@ -2,15 +2,15 @@ - **Documentation status:** Active-PR evidence dossier - **Canonical owner:** PR #44 (`docs: reconcile architecture documentation fitness`) -- **Protected-main baseline:** `67af7c87589edc2039545af335c95064d9b8391c` +- **Protected-main baseline:** `0c376acf059be9ddddddfbde1d0189e4f39ef014` - **Capability maturity:** **PARTIAL** - **Governing decision:** Proposed ADR 0013 separates Manifest V3 compatibility from OriginWeave Agent authority. ## 1. Why this dossier exists -Manifest V3 compatibility and OriginWeave Agent authority are intentionally different evidence domains. A Chromium extension may possess Chrome permissions and may be explicitly granted a narrow OriginWeave extension capability without receiving Agent origin grants, Agent action capability, instruction trust, secret-delivery authority, approval, or protected-value access. +Manifest V3 compatibility and OriginWeave Agent authority are intentionally different evidence domains. A Chromium extension may possess Chrome permissions and may be explicitly granted a narrow OriginWeave extension capability without receiving Agent origin grants, Agent action capability, instruction trust, secret-delivery authority, approval, protected-value access, or ambient native-process authority. -This dossier records the current executable composition evidence for that separation. It does not promote active pull requests to protected-main shipped truth and it does not claim the trusted sensitive-data broker from issue #10 is complete. +This dossier records current executable composition evidence for that separation. It does not promote active pull requests to protected-main shipped truth and it does not claim the trusted sensitive-data broker from issue #10 or the native-host process adapter from issue #27 is complete. ## 2. Protected-main authority @@ -32,7 +32,7 @@ These foundations are **IMPLEMENTED_ON_PROTECTED_MAIN**. They do not by themselv **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -Exact head `a57873b3688984711918be17aadd348ed9fb12a9` proves that, after an extension is genuinely allowed to `ProposeTypedAction`: +Exact current head `e7265a86d63c9e5f047ed6d32c3988b01e53fa13` proves that, after an extension is genuinely allowed to `ProposeTypedAction`: 1. a proposed navigation outside the Agent readable-origin grant is still denied; 2. proposal permission cannot supply the missing Agent `Navigate` capability; @@ -46,13 +46,33 @@ The branch adds no production API and no extension runtime. It is compositional **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -Exact head `e83749acd1cf5a0b778ba38eb9d6ed5a9bd1e68f` deliberately keeps only the distinct approval-composition proof after duplicate regressions were removed in favor of PR #62 ownership. It proves that, after the same exact proposal grant is admitted and the Agent context independently possesses `FillSecret` plus exact readable/writable origin authority, broker-handle `FillSecret` still reaches the ordinary R3 approval boundary rather than becoming implicitly allowed. +Exact current head `a4595c393f459f57bfe2199ace44271f246751c4` deliberately keeps only the distinct approval-composition proof after duplicate regressions were removed in favor of PR #62 ownership. It proves that, after the same exact proposal grant is admitted and the Agent context independently possesses `FillSecret` plus exact readable/writable origin authority, broker-handle `FillSecret` still reaches the ordinary R3 approval boundary rather than becoming implicitly allowed. -The exact head has successful CI, exact owned production coverage, Security Scan, SAST and CodeRabbit status and is Ready for review. It has no raw secret bytes and does not create approval evidence, a broker, browser-fill adapter, protected-value store, KMS path, authenticated workload identity, persistence owner, or release claim. +This lane adds no broker, browser-fill adapter, protected-value store, KMS path, authenticated workload identity, persistence owner, or approval evidence. + +### PR #82 — exact extension-to-native-host authority + +**Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` + +Exact current head `427d2f32431139dc7ed59e60df00fd9d0c4eeba0` provides bounded native-host names plus exact extension-ID/host-name grants and request identity getters. Chrome `nativeMessaging` permission remains separate from OriginWeave Agent authority. The lane does not parse an installed host manifest, read operating-system registration, launch a process, frame stdio, or trust native-host output. + +### PR #154 — bounded native-messaging framing + +**Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` + +Exact current head `d2e1ae8d654703b76897db202980fec82d26babc`, stacked on #82, bounds native-endian message framing with direction-specific payload ceilings, exact frame length, and UTF-8 text validation before later JSON/untrusted-observation handling. It does not prove host-manifest installation, process ownership, sandboxing, stdio provenance, or Agent authority. + +### PR #169 — validated host-manifest authority + +**Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` + +Current Draft head `6d85f1a15e1501f48ce2d12d560323a36b38719b`, stacked on #154, adds test-first host-manifest authority. It accepts only exact `stdio`, requires a non-empty bounded raw `allowed_origins` list, validates exact canonical `chrome-extension:///` origins without wildcard or suffix normalization, collapses duplicate exact origins without widening authority, and allows only an exact host plus explicitly listed extension identity. + +The implementation intentionally treats caller-supplied validated manifest fields as one authority input only. It does not read JSON/filesystem/registry state, canonicalize or attest executable paths, prove installer/OS ownership, spawn/sandbox/supervise a host process, authenticate the stdio peer, parse/trust host JSON, expose protected values, or grant Agent actions. Those remain separately reviewed runtime boundaries. ## 4. Security interpretation -The executable authority chain is intentionally non-transitive: +The executable authority chains are intentionally non-transitive: ```text Chromium extension permission @@ -66,14 +86,29 @@ Chromium extension permission -/> protected-value resolution ``` -A future real extension adapter must preserve these separations. Chrome permissions and extension proposal grants are inputs to policy composition, never ambient authority that bypasses the deterministic Agent policy or the sensitive-data broker boundary. +and: + +```text +Chrome nativeMessaging permission +-> exact extension/host grant +-> validated exact host-manifest allow-list +-> bounded native-messaging framing +-/> installed-host ownership +-/> process identity / sandbox authority +-/> trusted message provenance +-/> Agent authority +-/> protected-value access +``` + +A future real extension/native-host adapter must preserve these separations. Chrome permissions, extension grants, host-manifest fields, and framed native bytes are inputs to explicit policy/provenance composition, never ambient authority that bypasses deterministic Agent or sensitive-data controls. ## 5. Remaining issue #27 / #10 boundary This dossier does **not** close issue #27 or issue #10. Remaining material work includes, among other accepted requirements: +- trusted platform-specific native-host registration discovery and ownership/path validation; +- process sandboxing, lifecycle supervision, authenticated stdio peer attribution, crash recovery, and untrusted-message handling; - real managed-extension allow-list and enterprise policy integration; -- native-messaging host boundary and process isolation; - complete supported-capability release matrix and regression gate; - authenticated workload/service identity for sensitive-data broker audience; - protected-value resolution/fill outside model-visible context; @@ -82,4 +117,4 @@ This dossier does **not** close issue #27 or issue #10. Remaining material work ## 6. Documentation fitness consequence -The existing ADR/PRD/TRD/Architecture/UML/ERD graph remains **DESIGN-SUFFICIENT / PROTECTED-MAIN-PARTIAL**. PRs #62 and #63 narrow distinct executable extension-authority evidence gaps without introducing a new trust domain, deployment component, persistence entity, database schema, or independent architecture decision. Proposed ADR 0013 remains Proposed until its own lifecycle authority changes. \ No newline at end of file +The existing ADR/PRD/TRD/Architecture/UML/ERD graph remains **DESIGN-SUFFICIENT / PROTECTED-MAIN-PARTIAL**. PRs #62, #63, #82, #154, and #169 narrow distinct executable extension/native-messaging authority gaps without introducing an OriginWeave-owned database schema or a new architectural trust domain beyond Proposed ADR 0013. ADR 0013 remains Proposed until its own lifecycle authority changes. \ No newline at end of file From ea89ba0dad1757d2cfe003538c03d814a2b8a4c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 15:31:25 +0900 Subject: [PATCH 08/64] test(extension): bind native host executable path --- .../native_messaging_manifest_authority.rs | 95 +++++++++++++++++-- 1 file changed, 89 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_authority.rs b/crates/originweave-core/tests/native_messaging_manifest_authority.rs index ffcfdf8cb..443552c7b 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_authority.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_authority.rs @@ -5,11 +5,12 @@ use std::error::Error; use originweave_core::{ ExtensionId, MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, NativeMessagingAccessRequest, NativeMessagingHostManifest, NativeMessagingHostManifestAccessDecision, - NativeMessagingHostManifestError, NativeMessagingHostName, + NativeMessagingHostManifestError, NativeMessagingHostName, NativeMessagingHostPlatform, }; const ALLOWED_EXTENSION: &str = "abcdefghijklmnopabcdefghijklmnop"; const OTHER_EXTENSION: &str = "bcdefghijklmnopabcdefghijklmnopa"; +const LINUX_HOST_PATH: &str = "/opt/originweave/native-host"; fn extension_id(value: &str) -> ExtensionId { ExtensionId::parse(value).expect("valid extension id") @@ -24,16 +25,20 @@ fn extension_origin(value: &str) -> String { } #[test] -fn manifest_binds_stdio_host_to_exact_allowed_extension_origins() -> Result<(), Box> { +fn manifest_binds_stdio_host_path_and_exact_allowed_extension_origins() -> Result<(), Box> { let host = host_name("com.contextualwisdom.originweave"); let allowed_origin = extension_origin(ALLOWED_EXTENSION); let manifest = NativeMessagingHostManifest::parse( host.clone(), + NativeMessagingHostPlatform::Linux, + LINUX_HOST_PATH, "stdio", &[allowed_origin.as_str(), allowed_origin.as_str()], )?; assert_eq!(manifest.host_name(), &host); + assert_eq!(manifest.platform(), NativeMessagingHostPlatform::Linux); + assert_eq!(manifest.executable_path(), LINUX_HOST_PATH); assert_eq!(manifest.allowed_extension_count(), 1); let exact = NativeMessagingAccessRequest::new(extension_id(ALLOWED_EXTENSION), host.clone()); @@ -60,23 +65,87 @@ fn manifest_binds_stdio_host_to_exact_allowed_extension_origins() -> Result<(), Ok(()) } +#[test] +fn manifest_enforces_platform_specific_executable_path_shape() -> Result<(), Box> { + let host = host_name("com.contextualwisdom.originweave"); + let allowed_origin = extension_origin(ALLOWED_EXTENSION); + + let windows = NativeMessagingHostManifest::parse( + host.clone(), + NativeMessagingHostPlatform::Windows, + "native-host.exe", + "stdio", + &[allowed_origin.as_str()], + )?; + assert_eq!(windows.platform(), NativeMessagingHostPlatform::Windows); + assert_eq!(windows.executable_path(), "native-host.exe"); + + for platform in [ + NativeMessagingHostPlatform::Linux, + NativeMessagingHostPlatform::MacOs, + ] { + assert_eq!( + NativeMessagingHostManifest::parse( + host.clone(), + platform, + "relative/native-host", + "stdio", + &[allowed_origin.as_str()], + ), + Err(NativeMessagingHostManifestError::RelativeExecutablePathUnsupported) + ); + } + + for invalid_path in ["", "bad\0path"] { + assert_eq!( + NativeMessagingHostManifest::parse( + host.clone(), + NativeMessagingHostPlatform::Windows, + invalid_path, + "stdio", + &[allowed_origin.as_str()], + ), + Err(NativeMessagingHostManifestError::InvalidExecutablePath) + ); + } + Ok(()) +} + #[test] fn manifest_rejects_non_stdio_empty_and_oversized_allowlists() { let host = host_name("com.contextualwisdom.originweave"); let allowed_origin = extension_origin(ALLOWED_EXTENSION); assert_eq!( - NativeMessagingHostManifest::parse(host.clone(), "pipe", &[allowed_origin.as_str()]), + NativeMessagingHostManifest::parse( + host.clone(), + NativeMessagingHostPlatform::Linux, + LINUX_HOST_PATH, + "pipe", + &[allowed_origin.as_str()], + ), Err(NativeMessagingHostManifestError::UnsupportedInterfaceType) ); assert_eq!( - NativeMessagingHostManifest::parse(host.clone(), "stdio", &[]), + NativeMessagingHostManifest::parse( + host.clone(), + NativeMessagingHostPlatform::Linux, + LINUX_HOST_PATH, + "stdio", + &[], + ), Err(NativeMessagingHostManifestError::MissingAllowedOrigin) ); let oversized = vec![allowed_origin.as_str(); MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS + 1]; assert_eq!( - NativeMessagingHostManifest::parse(host, "stdio", &oversized), + NativeMessagingHostManifest::parse( + host, + NativeMessagingHostPlatform::Linux, + LINUX_HOST_PATH, + "stdio", + &oversized, + ), Err(NativeMessagingHostManifestError::TooManyAllowedOrigins) ); } @@ -95,7 +164,13 @@ fn manifest_rejects_ambiguous_or_wildcard_extension_origins() { for invalid in invalid_origins { assert_eq!( - NativeMessagingHostManifest::parse(host.clone(), "stdio", &[invalid]), + NativeMessagingHostManifest::parse( + host.clone(), + NativeMessagingHostPlatform::Linux, + LINUX_HOST_PATH, + "stdio", + &[invalid], + ), Err(NativeMessagingHostManifestError::InvalidAllowedOrigin), "unexpected allowed origin: {invalid:?}" ); @@ -109,6 +184,14 @@ fn manifest_error_messages_are_deterministic_and_source_free() { NativeMessagingHostManifestError::UnsupportedInterfaceType, "native messaging host manifest interface type must be stdio", ), + ( + NativeMessagingHostManifestError::InvalidExecutablePath, + "native messaging host manifest contains an invalid executable path", + ), + ( + NativeMessagingHostManifestError::RelativeExecutablePathUnsupported, + "native messaging host executable path must be absolute on this platform", + ), ( NativeMessagingHostManifestError::MissingAllowedOrigin, "native messaging host manifest must allow at least one exact extension origin", From d1423e75189aa7388a5f287be9c30f425cf773ca Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 15:32:39 +0900 Subject: [PATCH 09/64] test(extension): apply canonical rustfmt --- .../tests/native_messaging_manifest_authority.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_authority.rs b/crates/originweave-core/tests/native_messaging_manifest_authority.rs index 443552c7b..f12efc798 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_authority.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_authority.rs @@ -25,7 +25,8 @@ fn extension_origin(value: &str) -> String { } #[test] -fn manifest_binds_stdio_host_path_and_exact_allowed_extension_origins() -> Result<(), Box> { +fn manifest_binds_stdio_host_path_and_exact_allowed_extension_origins() -> Result<(), Box> +{ let host = host_name("com.contextualwisdom.originweave"); let allowed_origin = extension_origin(ALLOWED_EXTENSION); let manifest = NativeMessagingHostManifest::parse( From cc6c6b1297539aad0bb23e9e6b4281bd5fbcd05f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 15:34:35 +0900 Subject: [PATCH 10/64] feat(extension): bind native host executable path --- .../src/native_messaging_manifest.rs | 78 +++++++++++++++++-- 1 file changed, 71 insertions(+), 7 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest.rs b/crates/originweave-core/src/native_messaging_manifest.rs index f44f8a97e..5d0cbdd06 100644 --- a/crates/originweave-core/src/native_messaging_manifest.rs +++ b/crates/originweave-core/src/native_messaging_manifest.rs @@ -18,33 +18,58 @@ use crate::{ExtensionId, NativeMessagingAccessRequest, NativeMessagingHostName}; /// manifest from turning policy admission into unbounded allocation or comparison work. pub const MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS: usize = 256; +/// 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 only the exact host identity and exact Chromium extension identities -/// named by the manifest's `allowed_origins`. Possessing this value is not proof of manifest -/// installation, executable ownership, process identity, message provenance, or Agent +/// The record contains the exact host identity, declared executable-path text and platform, +/// plus exact Chromium extension identities named by the manifest's `allowed_origins`. +/// Possessing this value is not proof of manifest installation, path canonicalization, +/// executable existence or ownership, process identity, message provenance, or Agent /// authority. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NativeMessagingHostManifest { host_name: NativeMessagingHostName, + platform: NativeMessagingHostPlatform, + executable_path: String, allowed_extensions: BTreeSet, } impl NativeMessagingHostManifest { /// Validate the authority-bearing host-manifest fields without widening them. /// - /// `interface_type` must be exactly `stdio`. Every allowed origin must be exactly - /// `chrome-extension:///`; 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. + /// `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 and embedded NUL bytes are rejected on every platform. + /// Every allowed origin must be exactly `chrome-extension:///`; + /// 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. pub fn parse( host_name: NativeMessagingHostName, + platform: NativeMessagingHostPlatform, + executable_path: &str, interface_type: &str, allowed_origins: &[&str], ) -> Result { if interface_type != "stdio" { return Err(NativeMessagingHostManifestError::UnsupportedInterfaceType); } + validate_executable_path(platform, executable_path)?; if allowed_origins.is_empty() { return Err(NativeMessagingHostManifestError::MissingAllowedOrigin); } @@ -59,6 +84,8 @@ impl NativeMessagingHostManifest { Ok(Self { host_name, + platform, + executable_path: executable_path.to_owned(), allowed_extensions, }) } @@ -69,6 +96,22 @@ impl NativeMessagingHostManifest { &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 { @@ -95,6 +138,19 @@ impl NativeMessagingHostManifest { } } +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 platform != NativeMessagingHostPlatform::Windows && !executable_path.starts_with('/') { + return Err(NativeMessagingHostManifestError::RelativeExecutablePathUnsupported); + } + Ok(()) +} + fn parse_extension_origin(origin: &str) -> Result { let Some(extension_text) = origin.strip_prefix("chrome-extension://") else { return Err(NativeMessagingHostManifestError::InvalidAllowedOrigin); @@ -122,6 +178,10 @@ pub enum NativeMessagingHostManifestAccessDecision { 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, + /// 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. @@ -135,6 +195,10 @@ impl fmt::Display for NativeMessagingHostManifestError { 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::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", ), From 0f781c4881ad863d539e8f1fae07a49089cbb8da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 15:34:53 +0900 Subject: [PATCH 11/64] feat(extension): expose native host platform authority --- crates/originweave-core/src/root.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs index a826263ec..0e8028c42 100644 --- a/crates/originweave-core/src/root.rs +++ b/crates/originweave-core/src/root.rs @@ -15,4 +15,5 @@ mod native_messaging_manifest; pub use native_messaging_manifest::{ MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, NativeMessagingHostManifest, NativeMessagingHostManifestAccessDecision, NativeMessagingHostManifestError, + NativeMessagingHostPlatform, }; From fdd674b6f0ea42d9114702d61cd3dce389f5b5a1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 15:40:47 +0900 Subject: [PATCH 12/64] test(extension): bound native host executable path --- .../native_messaging_manifest_authority.rs | 40 +++++++++++++++++-- 1 file changed, 37 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_authority.rs b/crates/originweave-core/tests/native_messaging_manifest_authority.rs index f12efc798..8d2909c2a 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_authority.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_authority.rs @@ -3,9 +3,10 @@ use std::error::Error; use originweave_core::{ - ExtensionId, MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, NativeMessagingAccessRequest, - NativeMessagingHostManifest, NativeMessagingHostManifestAccessDecision, - NativeMessagingHostManifestError, NativeMessagingHostName, NativeMessagingHostPlatform, + ExtensionId, MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, MAX_NATIVE_MESSAGING_EXECUTABLE_PATH_BYTES, + NativeMessagingAccessRequest, NativeMessagingHostManifest, + NativeMessagingHostManifestAccessDecision, NativeMessagingHostManifestError, + NativeMessagingHostName, NativeMessagingHostPlatform, }; const ALLOWED_EXTENSION: &str = "abcdefghijklmnopabcdefghijklmnop"; @@ -112,6 +113,35 @@ fn manifest_enforces_platform_specific_executable_path_shape() -> Result<(), Box Ok(()) } +#[test] +fn manifest_bounds_executable_path_before_storage() -> Result<(), Box> { + let host = host_name("com.contextualwisdom.originweave"); + let allowed_origin = extension_origin(ALLOWED_EXTENSION); + let exact_limit = "a".repeat(MAX_NATIVE_MESSAGING_EXECUTABLE_PATH_BYTES); + let one_over = "a".repeat(MAX_NATIVE_MESSAGING_EXECUTABLE_PATH_BYTES + 1); + + let accepted = NativeMessagingHostManifest::parse( + host.clone(), + NativeMessagingHostPlatform::Windows, + &exact_limit, + "stdio", + &[allowed_origin.as_str()], + )?; + assert_eq!(accepted.executable_path().len(), exact_limit.len()); + + assert_eq!( + NativeMessagingHostManifest::parse( + host, + NativeMessagingHostPlatform::Windows, + &one_over, + "stdio", + &[allowed_origin.as_str()], + ), + Err(NativeMessagingHostManifestError::ExecutablePathTooLong) + ); + Ok(()) +} + #[test] fn manifest_rejects_non_stdio_empty_and_oversized_allowlists() { let host = host_name("com.contextualwisdom.originweave"); @@ -189,6 +219,10 @@ fn manifest_error_messages_are_deterministic_and_source_free() { NativeMessagingHostManifestError::InvalidExecutablePath, "native messaging host manifest contains an invalid executable path", ), + ( + NativeMessagingHostManifestError::ExecutablePathTooLong, + "native messaging host manifest executable path exceeds the OriginWeave safety budget", + ), ( NativeMessagingHostManifestError::RelativeExecutablePathUnsupported, "native messaging host executable path must be absolute on this platform", From b6d2601bcfc0596c5f7819ffb7a9c7805bdd9adf Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 15:43:54 +0900 Subject: [PATCH 13/64] feat(extension): bound native host executable path --- .../src/native_messaging_manifest.rs | 20 +++++++++++++++++-- crates/originweave-core/src/root.rs | 6 +++--- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest.rs b/crates/originweave-core/src/native_messaging_manifest.rs index 5d0cbdd06..998eea3ff 100644 --- a/crates/originweave-core/src/native_messaging_manifest.rs +++ b/crates/originweave-core/src/native_messaging_manifest.rs @@ -18,6 +18,13 @@ use crate::{ExtensionId, NativeMessagingAccessRequest, NativeMessagingHostName}; /// 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 @@ -54,8 +61,9 @@ impl NativeMessagingHostManifest { /// `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 and embedded NUL bytes are rejected on every platform. - /// Every allowed origin must be exactly `chrome-extension:///`; + /// 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:///`; /// 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. @@ -145,6 +153,9 @@ fn validate_executable_path( 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); } @@ -180,6 +191,8 @@ pub enum NativeMessagingHostManifestError { 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. @@ -197,6 +210,9 @@ impl fmt::Display for NativeMessagingHostManifestError { .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( diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs index 0e8028c42..750f5a56c 100644 --- a/crates/originweave-core/src/root.rs +++ b/crates/originweave-core/src/root.rs @@ -13,7 +13,7 @@ pub use legacy_contracts::*; mod native_messaging_manifest; pub use native_messaging_manifest::{ - MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, NativeMessagingHostManifest, - NativeMessagingHostManifestAccessDecision, NativeMessagingHostManifestError, - NativeMessagingHostPlatform, + MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, MAX_NATIVE_MESSAGING_EXECUTABLE_PATH_BYTES, + NativeMessagingHostManifest, NativeMessagingHostManifestAccessDecision, + NativeMessagingHostManifestError, NativeMessagingHostPlatform, }; From 56a3a097a7027fe2f49d6180f6a0db8ce8436fa3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:39:33 +0900 Subject: [PATCH 14/64] test(extension): bound native manifest document ingress --- .../native_messaging_manifest_document.rs | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_manifest_document.rs diff --git a/crates/originweave-core/tests/native_messaging_manifest_document.rs b/crates/originweave-core/tests/native_messaging_manifest_document.rs new file mode 100644 index 000000000..abee2c1df --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_manifest_document.rs @@ -0,0 +1,55 @@ +#![allow(clippy::expect_used)] + +use std::error::Error; + +use originweave_core::{ + MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES, NativeMessagingManifestDocument, + NativeMessagingManifestDocumentError, +}; + +#[test] +fn native_messaging_manifest_document_is_bounded_before_text_storage() { + let exact_limit = vec![b' '; MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES]; + let document = NativeMessagingManifestDocument::parse(&exact_limit) + .expect("the exact OriginWeave manifest-document safety bound remains accepted"); + assert_eq!(document.as_str().len(), exact_limit.len()); + + let one_over = vec![b' '; MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES + 1]; + assert_eq!( + NativeMessagingManifestDocument::parse(&one_over), + Err(NativeMessagingManifestDocumentError::DocumentTooLarge) + ); +} + +#[test] +fn native_messaging_manifest_document_rejects_empty_and_invalid_utf8() { + assert_eq!( + NativeMessagingManifestDocument::parse(&[]), + Err(NativeMessagingManifestDocumentError::EmptyDocument) + ); + assert_eq!( + NativeMessagingManifestDocument::parse(&[0xff]), + Err(NativeMessagingManifestDocumentError::InvalidUtf8) + ); +} + +#[test] +fn native_messaging_manifest_document_errors_are_standard_and_source_free() { + for (error, expected) in [ + ( + NativeMessagingManifestDocumentError::EmptyDocument, + "native messaging host manifest document is empty", + ), + ( + NativeMessagingManifestDocumentError::DocumentTooLarge, + "native messaging host manifest document exceeds the OriginWeave safety budget", + ), + ( + NativeMessagingManifestDocumentError::InvalidUtf8, + "native messaging host manifest document is not valid UTF-8", + ), + ] { + assert_eq!(error.to_string(), expected); + assert!(error.source().is_none()); + } +} From 5d10c28ee89a50b0ae5880c21454630ceab883be Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:41:12 +0900 Subject: [PATCH 15/64] feat(extension): bound native manifest document ingress --- .../src/native_messaging_manifest_document.rs | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 crates/originweave-core/src/native_messaging_manifest_document.rs diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs new file mode 100644 index 000000000..793f7db1a --- /dev/null +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -0,0 +1,79 @@ +//! Bounded pre-parser ingress for one Chrome native-messaging host manifest document. +//! +//! This module deliberately stops before JSON parsing. It bounds untrusted document bytes and +//! validates UTF-8 before a future manifest parser can allocate from or interpret structured +//! fields. Admission here does not prove that the document is valid JSON, installed by Chrome, +//! authenticated by the operating system, or safe to use as process or Agent authority. + +use std::fmt; + +/// Maximum UTF-8 byte length accepted for one native-messaging host manifest document. +/// +/// Chrome does not define this OriginWeave-specific 64 KiB safety budget. The limit exists to +/// bound allocation and parser input before any JSON or authority-bearing field processing. +pub const MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES: usize = 64 * 1024; + +/// A bounded UTF-8 native-messaging host manifest document awaiting structured parsing. +/// +/// Possessing this value proves only that the original byte document was non-empty, within the +/// OriginWeave ingress budget, and valid UTF-8. It is not a validated host manifest and carries +/// no installation, origin, executable, process, or Agent authority. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct NativeMessagingManifestDocument { + text: String, +} + +impl NativeMessagingManifestDocument { + /// Admit one untrusted manifest document before JSON parsing. + /// + /// The byte-size check runs before UTF-8 decoding or allocation of the stored `String` so + /// oversized input cannot force unbounded parser or text-storage work. Empty input and + /// invalid UTF-8 fail closed. + pub fn parse(bytes: &[u8]) -> Result { + if bytes.is_empty() { + return Err(NativeMessagingManifestDocumentError::EmptyDocument); + } + if bytes.len() > MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES { + return Err(NativeMessagingManifestDocumentError::DocumentTooLarge); + } + let text = std::str::from_utf8(bytes) + .map_err(|_error| NativeMessagingManifestDocumentError::InvalidUtf8)?; + Ok(Self { + text: text.to_owned(), + }) + } + + /// Return the exact validated UTF-8 text without interpreting JSON fields. + #[must_use] + pub fn as_str(&self) -> &str { + &self.text + } +} + +/// Failure to admit a native-messaging host manifest document at the pre-parser boundary. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum NativeMessagingManifestDocumentError { + /// The manifest document contained zero bytes. + EmptyDocument, + /// The manifest document exceeded the OriginWeave pre-parser safety budget. + DocumentTooLarge, + /// The manifest document was not valid UTF-8. + InvalidUtf8, +} + +impl fmt::Display for NativeMessagingManifestDocumentError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::EmptyDocument => { + formatter.write_str("native messaging host manifest document is empty") + } + Self::DocumentTooLarge => formatter.write_str( + "native messaging host manifest document exceeds the OriginWeave safety budget", + ), + Self::InvalidUtf8 => formatter + .write_str("native messaging host manifest document is not valid UTF-8"), + } + } +} + +impl std::error::Error for NativeMessagingManifestDocumentError {} From 6a3bf02d6369b41a69ec83c4455d1a0a87161676 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:41:31 +0900 Subject: [PATCH 16/64] feat(extension): expose bounded manifest document ingress --- crates/originweave-core/src/root.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs index f60dd11f8..fafe28e36 100644 --- a/crates/originweave-core/src/root.rs +++ b/crates/originweave-core/src/root.rs @@ -31,3 +31,9 @@ pub use native_messaging_manifest::{ NativeMessagingHostManifest, NativeMessagingHostManifestAccessDecision, NativeMessagingHostManifestError, NativeMessagingHostPlatform, }; + +mod native_messaging_manifest_document; +pub use native_messaging_manifest_document::{ + MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES, NativeMessagingManifestDocument, + NativeMessagingManifestDocumentError, +}; From d15a0014742d1c0d01323c9c3575fcef513f5ce1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:44:14 +0900 Subject: [PATCH 17/64] style(extension): apply canonical manifest ingress formatting --- .../src/native_messaging_manifest_document.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index 793f7db1a..c6921af48 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -70,8 +70,9 @@ impl fmt::Display for NativeMessagingManifestDocumentError { Self::DocumentTooLarge => formatter.write_str( "native messaging host manifest document exceeds the OriginWeave safety budget", ), - Self::InvalidUtf8 => formatter - .write_str("native messaging host manifest document is not valid UTF-8"), + Self::InvalidUtf8 => { + formatter.write_str("native messaging host manifest document is not valid UTF-8") + } } } } From 6218c54a57e463962970148dea04a4dd68b89295 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:47:39 +0900 Subject: [PATCH 18/64] docs(extension): record bounded manifest document ingress --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index e1cbfa2c6..897c458ff 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,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. - 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. -- Fail-closed native-messaging host-manifest authority that accepts only exact `stdio`, bounds raw `allowed_origins` before deduplication, validates only exact `chrome-extension:///` entries, and matches exact host/extension identity without treating manifest installation or process state as Agent authority. +- Fail-closed native-messaging host-manifest authority with a 64 KiB non-empty UTF-8 pre-parser document-ingress budget, exact `stdio`, bounded raw `allowed_origins` before deduplication, exact `chrome-extension:///` entries, bounded executable-path text, and exact host/extension matching without treating document admission, manifest installation, or process state as 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. From 37d0d3067ae2d894351c6221e371130be7a613f3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 21:49:31 +0900 Subject: [PATCH 19/64] docs(extension): trace bounded manifest ingress authority --- docs/traceability/extension-authority-security.md | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/docs/traceability/extension-authority-security.md b/docs/traceability/extension-authority-security.md index 7375fc451..9de0dea22 100644 --- a/docs/traceability/extension-authority-security.md +++ b/docs/traceability/extension-authority-security.md @@ -60,21 +60,21 @@ The current origin-binding slice requires `ExtensionAgentGrant` and `ExtensionAc **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -Exact current head `ad1ece96eee7209d27d7fe87001832c412ec71f1` provides bounded native-host names plus exact extension-ID/host-name grants and request identity getters. Chrome `nativeMessaging` permission remains separate from OriginWeave Agent authority. The lane does not parse an installed host manifest, read operating-system registration, launch a process, frame stdio, or trust native-host output. +Exact current head `c639cd78e3acad235be4cbbfdef67b84ce7ddbfa` provides bounded native-host names plus exact extension-ID/host-name grants and request identity getters. Chrome `nativeMessaging` permission remains separate from OriginWeave Agent authority. The lane does not parse an installed host manifest, read operating-system registration, launch a process, frame stdio, or trust native-host output. ### PR #154 — bounded native-messaging framing **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -Exact current head `276e0d46408da57556a360c6cc2883a632312fea`, stacked on #82, preserves current extension authority while bounding native-endian message framing with direction-specific payload ceilings, exact frame length, and UTF-8 text validation before later JSON/untrusted-observation handling. It does not prove host-manifest installation, process ownership, sandboxing, stdio provenance, or Agent authority. +Exact current head `4a71b7dd357974f791f8d7f4a0be5c4c0b9ea1b1`, stacked on #82, preserves current extension authority while bounding native-endian message framing with direction-specific payload ceilings, exact frame length, and UTF-8 text validation before later JSON/untrusted-observation handling. It does not prove host-manifest installation, process ownership, sandboxing, stdio provenance, or Agent authority. ### PR #169 — validated host-manifest authority **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -This Draft, stacked on #154, adds test-first host-manifest authority. It accepts only exact `stdio`, requires a non-empty bounded raw `allowed_origins` list, validates exact canonical `chrome-extension:///` origins without wildcard or suffix normalization, collapses duplicate exact origins without widening authority, bounds executable-path allocation, preserves platform-specific path semantics, and allows only an exact host plus explicitly listed extension identity. +This Draft, stacked on #154, adds test-first host-manifest authority. Before future structured parsing, the raw manifest document must be non-empty valid UTF-8 and at most the OriginWeave 64 KiB pre-parser safety budget; the byte bound is enforced before storing the document as a `String`. This budget is an OriginWeave resource-governance limit, not a Chrome or operating-system manifest-size claim, and successful admission does not establish JSON validity or authority. The existing structured contract accepts only exact `stdio`, requires a non-empty bounded raw `allowed_origins` list, validates exact canonical `chrome-extension:///` origins without wildcard or suffix normalization, collapses duplicate exact origins without widening authority, bounds executable-path allocation, preserves platform-specific path semantics, and allows only an exact host plus explicitly listed extension identity. -The implementation intentionally treats caller-supplied validated manifest fields as one authority input only. It does not read JSON/filesystem/registry state, canonicalize or attest executable paths, prove installer/OS ownership, spawn/sandbox/supervise a host process, authenticate the stdio peer, parse/trust host JSON, expose protected values, or grant Agent actions. Those remain separately reviewed runtime boundaries. +The implementation intentionally treats bounded document admission and caller-supplied validated manifest fields as separate preconditions rather than ambient authority. It does not parse JSON from the admitted document, read or authenticate filesystem/registry registration, canonicalize or attest executable paths, resolve a Windows relative path against an authenticated manifest directory, prove installer/OS ownership, spawn/sandbox/supervise a host process, authenticate the stdio peer, parse/trust host JSON, expose protected values, or grant Agent actions. Those remain separately reviewed runtime boundaries. ## 4. Security interpretation @@ -97,22 +97,23 @@ and: ```text Chrome nativeMessaging permission -> exact extension/host grant +-> bounded UTF-8 manifest-document ingress -> validated exact host-manifest allow-list -> bounded native-messaging framing --/> installed-host ownership +-/> JSON validity / installed-host ownership -/> process identity / sandbox authority -/> trusted message provenance -/> Agent authority -/> protected-value access ``` -A future real extension/native-host adapter must preserve these separations. Chrome permissions, extension grants, host-manifest fields, and framed native bytes are inputs to explicit policy/provenance composition, never ambient authority that bypasses deterministic Agent or sensitive-data controls. +A future real extension/native-host adapter must preserve these separations. Chrome permissions, extension grants, bounded document bytes, validated host-manifest fields, and framed native bytes are inputs to explicit policy/provenance composition, never ambient authority that bypasses deterministic Agent or sensitive-data controls. ## 5. Remaining issue #27 / #10 boundary This dossier does **not** close issue #27 or issue #10. Remaining material work includes, among other accepted requirements: -- trusted platform-specific native-host registration discovery and ownership/path validation; +- structured JSON parsing with bounded field extraction plus trusted platform-specific native-host registration discovery and ownership/path validation; - process sandboxing, lifecycle supervision, authenticated stdio peer attribution, crash recovery, and untrusted-message handling; - real managed-extension allow-list and enterprise policy integration; - complete supported-capability release matrix and regression gate; From 8e611661fa913c82de7476b078f448d98b6ff8b2 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:22:16 +0900 Subject: [PATCH 20/64] test(extension): require native-initiated manifest declaration --- .../native_messaging_manifest_authority.rs | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/crates/originweave-core/tests/native_messaging_manifest_authority.rs b/crates/originweave-core/tests/native_messaging_manifest_authority.rs index 8d2909c2a..0e7dff59e 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_authority.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_authority.rs @@ -67,6 +67,33 @@ fn manifest_binds_stdio_host_path_and_exact_allowed_extension_origins() -> Resul Ok(()) } +#[test] +fn manifest_records_native_initiated_connection_declaration_without_granting_it() +-> Result<(), Box> { + let host = host_name("com.contextualwisdom.originweave"); + let allowed_origin = extension_origin(ALLOWED_EXTENSION); + + let declared = NativeMessagingHostManifest::parse_with_native_initiated_connections( + host.clone(), + NativeMessagingHostPlatform::Linux, + LINUX_HOST_PATH, + "stdio", + true, + &[allowed_origin.as_str()], + )?; + assert!(declared.supports_native_initiated_connections()); + + let absent = NativeMessagingHostManifest::parse( + host, + NativeMessagingHostPlatform::Linux, + LINUX_HOST_PATH, + "stdio", + &[allowed_origin.as_str()], + )?; + assert!(!absent.supports_native_initiated_connections()); + Ok(()) +} + #[test] fn manifest_enforces_platform_specific_executable_path_shape() -> Result<(), Box> { let host = host_name("com.contextualwisdom.originweave"); From 45136e4ea6b5b41916386907febfeed35572098a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:24:19 +0900 Subject: [PATCH 21/64] feat(extension): retain native-initiated manifest declaration --- .../src/native_messaging_manifest.rs | 49 +++++++++++++++++-- 1 file changed, 45 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest.rs b/crates/originweave-core/src/native_messaging_manifest.rs index 998eea3ff..ca6606dc6 100644 --- a/crates/originweave-core/src/native_messaging_manifest.rs +++ b/crates/originweave-core/src/native_messaging_manifest.rs @@ -43,9 +43,10 @@ pub enum NativeMessagingHostPlatform { /// Validated authority-bearing fields from one Chrome native-messaging host manifest. /// /// The record contains the exact host identity, declared executable-path text and platform, -/// plus exact Chromium extension identities named by the manifest's `allowed_origins`. -/// Possessing this value is not proof of manifest installation, path canonicalization, -/// executable existence or ownership, process identity, message provenance, or Agent +/// 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 { @@ -53,11 +54,34 @@ pub struct NativeMessagingHostManifest { platform: NativeMessagingHostPlatform, executable_path: String, allowed_extensions: BTreeSet, + 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::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 @@ -67,11 +91,17 @@ impl NativeMessagingHostManifest { /// 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. - pub fn parse( + /// + /// `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 { if interface_type != "stdio" { @@ -95,6 +125,7 @@ impl NativeMessagingHostManifest { platform, executable_path: executable_path.to_owned(), allowed_extensions, + supports_native_initiated_connections, }) } @@ -126,6 +157,16 @@ impl NativeMessagingHostManifest { 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 From 14f44520fc787f90346d4d96063444ae9e1164f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:28:16 +0900 Subject: [PATCH 22/64] docs(changelog): record native-initiated manifest declaration --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 897c458ff..b86a29bb7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,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. - 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. -- Fail-closed native-messaging host-manifest authority with a 64 KiB non-empty UTF-8 pre-parser document-ingress budget, exact `stdio`, bounded raw `allowed_origins` before deduplication, exact `chrome-extension:///` entries, bounded executable-path text, and exact host/extension matching without treating document admission, manifest installation, or process state as Agent authority. +- Fail-closed native-messaging host-manifest authority with a 64 KiB non-empty UTF-8 pre-parser document-ingress budget, exact `stdio`, bounded raw `allowed_origins` before deduplication, exact `chrome-extension:///` entries, bounded executable-path text, exact host/extension matching, and explicit retention of the optional native-initiated-connection declaration without treating that declaration, document admission, manifest installation, or process state as 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. From db14d4363c0e8f3095443d12e7c56739b66bc4c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:29:03 +0900 Subject: [PATCH 23/64] docs(extension): trace native-initiated declaration boundary --- docs/traceability/extension-authority-security.md | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/docs/traceability/extension-authority-security.md b/docs/traceability/extension-authority-security.md index 9de0dea22..d1fc5a410 100644 --- a/docs/traceability/extension-authority-security.md +++ b/docs/traceability/extension-authority-security.md @@ -72,9 +72,9 @@ Exact current head `4a71b7dd357974f791f8d7f4a0be5c4c0b9ea1b1`, stacked on #82, p **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -This Draft, stacked on #154, adds test-first host-manifest authority. Before future structured parsing, the raw manifest document must be non-empty valid UTF-8 and at most the OriginWeave 64 KiB pre-parser safety budget; the byte bound is enforced before storing the document as a `String`. This budget is an OriginWeave resource-governance limit, not a Chrome or operating-system manifest-size claim, and successful admission does not establish JSON validity or authority. The existing structured contract accepts only exact `stdio`, requires a non-empty bounded raw `allowed_origins` list, validates exact canonical `chrome-extension:///` origins without wildcard or suffix normalization, collapses duplicate exact origins without widening authority, bounds executable-path allocation, preserves platform-specific path semantics, and allows only an exact host plus explicitly listed extension identity. +This Draft, stacked on #154, adds test-first host-manifest authority. Before future structured parsing, the raw manifest document must be non-empty valid UTF-8 and at most the OriginWeave 64 KiB pre-parser safety budget; the byte bound is enforced before storing the document as a `String`. This budget is an OriginWeave resource-governance limit, not a Chrome or operating-system manifest-size claim, and successful admission does not establish JSON validity or authority. The structured contract accepts only exact `stdio`, requires a non-empty bounded raw `allowed_origins` list, validates exact canonical `chrome-extension:///` origins without wildcard or suffix normalization, collapses duplicate exact origins without widening authority, bounds executable-path allocation, preserves platform-specific path semantics, allows only an exact host plus explicitly listed extension identity, and retains the optional boolean `supports_native_initiated_connections` declaration when a trusted structured parser supplies it. The compatibility constructor records that declaration as absent/false rather than inventing support. -The implementation intentionally treats bounded document admission and caller-supplied validated manifest fields as separate preconditions rather than ambient authority. It does not parse JSON from the admitted document, read or authenticate filesystem/registry registration, canonicalize or attest executable paths, resolve a Windows relative path against an authenticated manifest directory, prove installer/OS ownership, spawn/sandbox/supervise a host process, authenticate the stdio peer, parse/trust host JSON, expose protected values, or grant Agent actions. Those remain separately reviewed runtime boundaries. +The implementation intentionally treats bounded document admission and caller-supplied validated manifest fields as separate preconditions rather than ambient authority. A retained `supports_native_initiated_connections: true` value proves only that the validated manifest declared the optional Chromium field; it does not prove Chromium feature or enterprise-policy enablement, installed registration, executable/process identity, native-initiated connection provenance, message trust, or Agent authority. This slice still does not parse JSON from the admitted document, read or authenticate filesystem/registry registration, canonicalize or attest executable paths, resolve a Windows relative path against an authenticated manifest directory, prove installer/OS ownership, spawn/sandbox/supervise a host process, authenticate the stdio peer, parse/trust host JSON, expose protected values, or grant Agent actions. Those remain separately reviewed runtime boundaries. ## 4. Security interpretation @@ -99,7 +99,9 @@ Chrome nativeMessaging permission -> exact extension/host grant -> bounded UTF-8 manifest-document ingress -> validated exact host-manifest allow-list +-> optional native-initiated-connection declaration retained as data only -> bounded native-messaging framing +-/> Chromium feature / enterprise-policy enablement -/> JSON validity / installed-host ownership -/> process identity / sandbox authority -/> trusted message provenance @@ -107,7 +109,7 @@ Chrome nativeMessaging permission -/> protected-value access ``` -A future real extension/native-host adapter must preserve these separations. Chrome permissions, extension grants, bounded document bytes, validated host-manifest fields, and framed native bytes are inputs to explicit policy/provenance composition, never ambient authority that bypasses deterministic Agent or sensitive-data controls. +A future real extension/native-host adapter must preserve these separations. Chrome permissions, extension grants, bounded document bytes, validated host-manifest fields, optional connection-direction declarations, and framed native bytes are inputs to explicit policy/provenance composition, never ambient authority that bypasses deterministic Agent or sensitive-data controls. ## 5. Remaining issue #27 / #10 boundary @@ -115,7 +117,7 @@ This dossier does **not** close issue #27 or issue #10. Remaining material work - structured JSON parsing with bounded field extraction plus trusted platform-specific native-host registration discovery and ownership/path validation; - process sandboxing, lifecycle supervision, authenticated stdio peer attribution, crash recovery, and untrusted-message handling; -- real managed-extension allow-list and enterprise policy integration; +- real managed-extension allow-list and enterprise policy integration, including independent validation of any Chromium native-initiated-connection feature/policy state before use; - complete supported-capability release matrix and regression gate; - authenticated workload/service identity for sensitive-data broker audience; - protected-value resolution/fill outside model-visible context; From 3bb3d98ed28d80fa0806d4497a5364ca52b7c3ae Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:30:48 +0900 Subject: [PATCH 24/64] docs(research): pin native-initiated manifest evidence --- docs/doctoring.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/doctoring.md b/docs/doctoring.md index c29f97ece..f9fc3d130 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -12,6 +12,8 @@ The 1 June 2026 WebDriver BiDi Working Draft defines a bidirectional remote-cont Chrome's current native-messaging documentation defines the host manifest as a separate configuration boundary containing an exact host `name`, executable `path`, interface `type`, and extension `allowed_origins`. The only documented interface type is `stdio`, and `allowed_origins` does not permit wildcard extension origins. Chrome starts the native host as a separate process and communicates through standard input and standard output. OriginWeave therefore treats validated host-manifest identity as one explicit authority input rather than as proof of installation, executable ownership, process identity, message provenance, or Agent authority. The first manifest contract accepts only exact `stdio`, exact canonical `chrome-extension:///` origins, and a bounded raw allow-list before deduplication; process registration, path ownership, spawning, sandboxing, and authenticated stdio remain separate boundaries. +Current Chromium source adds one compatibility detail not exposed as ambient authority: when the `kOnConnectNative` feature is enabled, the native-messaging host-manifest loader accepts an optional boolean `supports_native_initiated_connections` field and retains it on the parsed manifest. OriginWeave therefore retains that declaration when supplied by a trusted structured parser, while treating absence as false and keeping Chromium feature/enterprise-policy enablement, installed-host identity, process provenance, message provenance, and Agent authority as independent fail-closed boundaries. The reviewed Chromium source is pinned to revision `ed16ea6eb58194943d42c14aa385409203782aa9`; a mutable branch tip is not release 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`. @@ -114,6 +116,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 messaging host manifest parser* [Source code]. Chromium. https://chromium.googlesource.com/chromium/src/+/ed16ea6eb58194943d42c14aa385409203782aa9/chrome/browser/extensions/api/messaging/native_messaging_host_manifest.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 From 80006159ffb20531bb2542292500f4ef53e19d02 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:31:24 -0700 Subject: [PATCH 25/64] test(extension): require object-shaped native host manifest --- .../native_messaging_manifest_document.rs | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_document.rs b/crates/originweave-core/tests/native_messaging_manifest_document.rs index abee2c1df..d5d55c5f5 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_document.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_document.rs @@ -9,7 +9,9 @@ use originweave_core::{ #[test] fn native_messaging_manifest_document_is_bounded_before_text_storage() { - let exact_limit = vec![b' '; MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES]; + let mut exact_limit = vec![b' '; MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES]; + exact_limit[0] = b'{'; + exact_limit[MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES - 1] = b'}'; let document = NativeMessagingManifestDocument::parse(&exact_limit) .expect("the exact OriginWeave manifest-document safety bound remains accepted"); assert_eq!(document.as_str().len(), exact_limit.len()); @@ -33,6 +35,22 @@ fn native_messaging_manifest_document_rejects_empty_and_invalid_utf8() { ); } +#[test] +fn native_messaging_manifest_document_requires_outer_object_boundary() { + assert_eq!( + NativeMessagingManifestDocument::parse(b"[]"), + Err(NativeMessagingManifestDocumentError::InvalidObjectBoundary) + ); + assert_eq!( + NativeMessagingManifestDocument::parse(b"{"), + Err(NativeMessagingManifestDocumentError::InvalidObjectBoundary) + ); + + let document = NativeMessagingManifestDocument::parse(b" \r\n{\n}\t ") + .expect("JSON whitespace around an object-shaped document remains accepted"); + assert_eq!(document.as_str(), " \r\n{\n}\t "); +} + #[test] fn native_messaging_manifest_document_errors_are_standard_and_source_free() { for (error, expected) in [ @@ -48,6 +66,10 @@ fn native_messaging_manifest_document_errors_are_standard_and_source_free() { NativeMessagingManifestDocumentError::InvalidUtf8, "native messaging host manifest document is not valid UTF-8", ), + ( + NativeMessagingManifestDocumentError::InvalidObjectBoundary, + "native messaging host manifest document must have one outer JSON object boundary", + ), ] { assert_eq!(error.to_string(), expected); assert!(error.source().is_none()); From 4c2801c72d4380f42e24825b11b7cefff7fa0f53 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:33:03 -0700 Subject: [PATCH 26/64] fix(extension): bound native host manifest object envelope --- .../src/native_messaging_manifest_document.rs | 29 ++++++++++++++----- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index c6921af48..cfdd127a4 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -1,9 +1,10 @@ //! Bounded pre-parser ingress for one Chrome native-messaging host manifest document. //! -//! This module deliberately stops before JSON parsing. It bounds untrusted document bytes and -//! validates UTF-8 before a future manifest parser can allocate from or interpret structured -//! fields. Admission here does not prove that the document is valid JSON, installed by Chrome, -//! authenticated by the operating system, or safe to use as process or Agent authority. +//! This module deliberately stops before JSON parsing. It bounds untrusted document bytes, +//! validates UTF-8, and requires one outer object-shaped envelope using only JSON whitespace +//! before a future manifest parser can allocate from or interpret structured fields. Admission +//! here does not prove that the document is valid JSON, installed by Chrome, authenticated by +//! the operating system, or safe to use as process or Agent authority. use std::fmt; @@ -16,8 +17,9 @@ pub const MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES: usize = 64 * 1024; /// A bounded UTF-8 native-messaging host manifest document awaiting structured parsing. /// /// Possessing this value proves only that the original byte document was non-empty, within the -/// OriginWeave ingress budget, and valid UTF-8. It is not a validated host manifest and carries -/// no installation, origin, executable, process, or Agent authority. +/// OriginWeave ingress budget, valid UTF-8, and wrapped by one outer object boundary after JSON +/// whitespace is ignored. It is not proof of valid JSON and is not a validated host manifest; +/// it carries no installation, origin, executable, process, or Agent authority. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NativeMessagingManifestDocument { text: String, @@ -27,8 +29,10 @@ impl NativeMessagingManifestDocument { /// Admit one untrusted manifest document before JSON parsing. /// /// The byte-size check runs before UTF-8 decoding or allocation of the stored `String` so - /// oversized input cannot force unbounded parser or text-storage work. Empty input and - /// invalid UTF-8 fail closed. + /// oversized input cannot force unbounded parser or text-storage work. Empty input, invalid + /// UTF-8, and documents whose first and last non-JSON-whitespace characters are not `{` and + /// `}` fail closed. The object-envelope check is only a cheap ingress guard; a later + /// structured parser must still prove complete JSON syntax and field semantics. pub fn parse(bytes: &[u8]) -> Result { if bytes.is_empty() { return Err(NativeMessagingManifestDocumentError::EmptyDocument); @@ -38,6 +42,10 @@ impl NativeMessagingManifestDocument { } let text = std::str::from_utf8(bytes) .map_err(|_error| NativeMessagingManifestDocumentError::InvalidUtf8)?; + let trimmed = text.trim_matches(|character| matches!(character, ' ' | '\t' | '\r' | '\n')); + if !trimmed.starts_with('{') || !trimmed.ends_with('}') { + return Err(NativeMessagingManifestDocumentError::InvalidObjectBoundary); + } Ok(Self { text: text.to_owned(), }) @@ -59,6 +67,8 @@ pub enum NativeMessagingManifestDocumentError { DocumentTooLarge, /// The manifest document was not valid UTF-8. InvalidUtf8, + /// The document did not have one outer object boundary after JSON whitespace was removed. + InvalidObjectBoundary, } impl fmt::Display for NativeMessagingManifestDocumentError { @@ -73,6 +83,9 @@ impl fmt::Display for NativeMessagingManifestDocumentError { Self::InvalidUtf8 => { formatter.write_str("native messaging host manifest document is not valid UTF-8") } + Self::InvalidObjectBoundary => formatter.write_str( + "native messaging host manifest document must have one outer JSON object boundary", + ), } } } From cbf5089224f15a2aaa9d0be4f8932e558da18bd6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 22:37:39 -0700 Subject: [PATCH 27/64] docs(extension): trace native manifest envelope guard --- docs/traceability/extension-authority-security.md | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/traceability/extension-authority-security.md b/docs/traceability/extension-authority-security.md index d1fc5a410..3cb1e8bf2 100644 --- a/docs/traceability/extension-authority-security.md +++ b/docs/traceability/extension-authority-security.md @@ -32,7 +32,7 @@ These foundations are **IMPLEMENTED_ON_PROTECTED_MAIN**. They do not by themselv **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -Exact current head `e7265a86d63c9e5f047ed6d32c3988b01e53fa13` proves that, after an extension is genuinely allowed to `ProposeTypedAction`: +Exact current head `3690bf0a351b77957071f5399e9a31cec5f39e0b` proves that, after an extension is genuinely allowed to `ProposeTypedAction`: 1. a proposed navigation outside the Agent readable-origin grant is still denied; 2. proposal permission cannot supply the missing Agent `Navigate` capability; @@ -46,7 +46,7 @@ The branch adds no production API and no extension runtime. It is compositional **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -Exact current head `a4595c393f459f57bfe2199ace44271f246751c4` deliberately keeps only the distinct approval-composition proof after duplicate regressions were removed in favor of PR #62 ownership. It proves that, after the same exact proposal grant is admitted and the Agent context independently possesses `FillSecret` plus exact readable/writable origin authority, broker-handle `FillSecret` still reaches the ordinary R3 approval boundary rather than becoming implicitly allowed. +Exact current head `b80963fb81bed1ac4a01c1118f498af9765e2b79` deliberately keeps only the distinct approval-composition proof after duplicate regressions were removed in favor of PR #62 ownership. It proves that, after the same exact proposal grant is admitted and the Agent context independently possesses `FillSecret` plus exact readable/writable origin authority, broker-handle `FillSecret` still reaches the ordinary R3 approval boundary rather than becoming implicitly allowed. This lane adds no broker, browser-fill adapter, protected-value store, KMS path, authenticated workload identity, persistence owner, or approval evidence. @@ -72,9 +72,9 @@ Exact current head `4a71b7dd357974f791f8d7f4a0be5c4c0b9ea1b1`, stacked on #82, p **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -This Draft, stacked on #154, adds test-first host-manifest authority. Before future structured parsing, the raw manifest document must be non-empty valid UTF-8 and at most the OriginWeave 64 KiB pre-parser safety budget; the byte bound is enforced before storing the document as a `String`. This budget is an OriginWeave resource-governance limit, not a Chrome or operating-system manifest-size claim, and successful admission does not establish JSON validity or authority. The structured contract accepts only exact `stdio`, requires a non-empty bounded raw `allowed_origins` list, validates exact canonical `chrome-extension:///` origins without wildcard or suffix normalization, collapses duplicate exact origins without widening authority, bounds executable-path allocation, preserves platform-specific path semantics, allows only an exact host plus explicitly listed extension identity, and retains the optional boolean `supports_native_initiated_connections` declaration when a trusted structured parser supplies it. The compatibility constructor records that declaration as absent/false rather than inventing support. +This Draft, stacked on #154, adds test-first host-manifest authority. Before future structured parsing, the raw manifest document must be non-empty valid UTF-8, at most the OriginWeave 64 KiB pre-parser safety budget, and wrapped by one outer object-shaped envelope after the four JSON whitespace characters are ignored; the byte bound is enforced before storing the document as a `String`. The envelope check is deliberately only an ingress guard: it does not establish complete JSON syntax or field validity. The budget is an OriginWeave resource-governance limit, not a Chrome or operating-system manifest-size claim, and successful admission does not establish authority. The structured contract accepts only exact `stdio`, requires a non-empty bounded raw `allowed_origins` list, validates exact canonical `chrome-extension:///` origins without wildcard or suffix normalization, collapses duplicate exact origins without widening authority, bounds executable-path allocation, preserves platform-specific path semantics, allows only an exact host plus explicitly listed extension identity, and retains the optional boolean `supports_native_initiated_connections` declaration when a trusted structured parser supplies it. The compatibility constructor records that declaration as absent/false rather than inventing support. -The implementation intentionally treats bounded document admission and caller-supplied validated manifest fields as separate preconditions rather than ambient authority. A retained `supports_native_initiated_connections: true` value proves only that the validated manifest declared the optional Chromium field; it does not prove Chromium feature or enterprise-policy enablement, installed registration, executable/process identity, native-initiated connection provenance, message trust, or Agent authority. This slice still does not parse JSON from the admitted document, read or authenticate filesystem/registry registration, canonicalize or attest executable paths, resolve a Windows relative path against an authenticated manifest directory, prove installer/OS ownership, spawn/sandbox/supervise a host process, authenticate the stdio peer, parse/trust host JSON, expose protected values, or grant Agent actions. Those remain separately reviewed runtime boundaries. +The implementation intentionally treats bounded document admission and caller-supplied validated manifest fields as separate preconditions rather than ambient authority. A retained `supports_native_initiated_connections: true` value proves only that the validated manifest declared the optional Chromium field; it does not prove Chromium feature or enterprise-policy enablement, installed registration, executable/process identity, native-initiated connection provenance, message trust, or Agent authority. This slice still does not completely parse or validate JSON from the admitted document, read or authenticate filesystem/registry registration, canonicalize or attest executable paths, resolve a Windows relative path against an authenticated manifest directory, prove installer/OS ownership, spawn/sandbox/supervise a host process, authenticate the stdio peer, parse/trust host JSON, expose protected values, or grant Agent actions. Those remain separately reviewed runtime boundaries. ## 4. Security interpretation @@ -97,12 +97,12 @@ and: ```text Chrome nativeMessaging permission -> exact extension/host grant --> bounded UTF-8 manifest-document ingress +-> bounded UTF-8, object-shaped manifest-document ingress -> validated exact host-manifest allow-list -> optional native-initiated-connection declaration retained as data only -> bounded native-messaging framing -/> Chromium feature / enterprise-policy enablement --/> JSON validity / installed-host ownership +-/> complete JSON validity / installed-host ownership -/> process identity / sandbox authority -/> trusted message provenance -/> Agent authority @@ -115,7 +115,7 @@ A future real extension/native-host adapter must preserve these separations. Chr This dossier does **not** close issue #27 or issue #10. Remaining material work includes, among other accepted requirements: -- structured JSON parsing with bounded field extraction plus trusted platform-specific native-host registration discovery and ownership/path validation; +- complete structured JSON parsing with bounded field extraction plus trusted platform-specific native-host registration discovery and ownership/path validation; - process sandboxing, lifecycle supervision, authenticated stdio peer attribution, crash recovery, and untrusted-message handling; - real managed-extension allow-list and enterprise policy integration, including independent validation of any Chromium native-initiated-connection feature/policy state before use; - complete supported-capability release matrix and regression gate; From 62a33289d2663f9a8237f68ccc31ad75e56bacb6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:32:56 -0700 Subject: [PATCH 28/64] test(core): require complete native manifest parsing --- .../native_messaging_manifest_document.rs | 160 +++++++++++++++++- 1 file changed, 158 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_document.rs b/crates/originweave-core/tests/native_messaging_manifest_document.rs index d5d55c5f5..a8adf913e 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_document.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_document.rs @@ -3,10 +3,14 @@ use std::error::Error; use originweave_core::{ - MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES, NativeMessagingManifestDocument, - NativeMessagingManifestDocumentError, + MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES, NativeMessagingHostManifestError, + NativeMessagingHostPlatform, NativeMessagingManifestDocument, + NativeMessagingManifestDocumentError, NativeMessagingManifestParseError, }; +const ALLOWED_EXTENSION: &str = "abcdefghijklmnopabcdefghijklmnop"; +const LINUX_HOST_PATH: &str = "/opt/originweave/native-host"; + #[test] fn native_messaging_manifest_document_is_bounded_before_text_storage() { let mut exact_limit = vec![b' '; MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES]; @@ -51,6 +55,158 @@ fn native_messaging_manifest_document_requires_outer_object_boundary() { assert_eq!(document.as_str(), " \r\n{\n}\t "); } +#[test] +fn native_messaging_manifest_document_parses_complete_authority_fields() -> Result<(), Box> { + let json = format!( + r#"{{ + "name": "com.contextualwisdom.originweave", + "description": "OriginWeave native host", + "path": "{LINUX_HOST_PATH}", + "type": "stdio", + "allowed_origins": ["chrome-extension://{ALLOWED_EXTENSION}/"], + "supports_native_initiated_connections": true + }}"# + ); + let document = NativeMessagingManifestDocument::parse(json.as_bytes())?; + let manifest = document.parse_host_manifest(NativeMessagingHostPlatform::Linux)?; + + assert_eq!(manifest.host_name().as_str(), "com.contextualwisdom.originweave"); + assert_eq!(manifest.platform(), NativeMessagingHostPlatform::Linux); + assert_eq!(manifest.executable_path(), LINUX_HOST_PATH); + assert_eq!(manifest.allowed_extension_count(), 1); + assert!(manifest.supports_native_initiated_connections()); + Ok(()) +} + +#[test] +fn native_messaging_manifest_document_decodes_json_string_escapes_before_validation() +-> Result<(), Box> { + let json = format!( + r#"{{ + "name": "com.contextualwisdom.origin\u0077eave", + "description": "Origin\\Weave \"native\" host", + "path": "\/opt\/originweave\/native-host", + "type": "st\u0064io", + "allowed_origins": ["chrome-extension:\/\/{ALLOWED_EXTENSION}\/"], + "supports_native_initiated_connections": false + }}"# + ); + let document = NativeMessagingManifestDocument::parse(json.as_bytes())?; + let manifest = document.parse_host_manifest(NativeMessagingHostPlatform::Linux)?; + + assert_eq!(manifest.host_name().as_str(), "com.contextualwisdom.originweave"); + assert_eq!(manifest.executable_path(), LINUX_HOST_PATH); + assert!(!manifest.supports_native_initiated_connections()); + Ok(()) +} + +#[test] +fn native_messaging_manifest_document_rejects_incomplete_or_ambiguous_json() { + for malformed in [ + r#"{"name":"com.contextualwisdom.originweave",}"#, + r#"{"name":"com.contextualwisdom.originweave" "description":"missing comma"}"#, + r#"{"name":"com.contextualwisdom.originweave","description":"bad\q"}"#, + r#"{"name":"com.contextualwisdom.originweave","description":"bad\uD800"}"#, + ] { + let document = NativeMessagingManifestDocument::parse(malformed.as_bytes()) + .expect("the pre-parser only proves the outer object boundary"); + assert_eq!( + document.parse_host_manifest(NativeMessagingHostPlatform::Linux), + Err(NativeMessagingManifestParseError::InvalidJson), + "unexpected malformed document: {malformed}" + ); + } +} + +#[test] +fn native_messaging_manifest_document_rejects_duplicate_unknown_missing_and_wrong_typed_fields() { + let duplicate = NativeMessagingManifestDocument::parse( + br#"{ + "name":"com.contextualwisdom.originweave", + "name":"com.contextualwisdom.other", + "description":"host", + "path":"/opt/originweave/native-host", + "type":"stdio", + "allowed_origins":["chrome-extension://abcdefghijklmnopabcdefghijklmnop/"] + }"#, + ) + .expect("outer object boundary remains valid"); + assert_eq!( + duplicate.parse_host_manifest(NativeMessagingHostPlatform::Linux), + Err(NativeMessagingManifestParseError::DuplicateField) + ); + + let unknown = NativeMessagingManifestDocument::parse( + br#"{ + "name":"com.contextualwisdom.originweave", + "description":"host", + "path":"/opt/originweave/native-host", + "type":"stdio", + "allowed_origins":["chrome-extension://abcdefghijklmnopabcdefghijklmnop/"], + "ambient_authority":true + }"#, + ) + .expect("outer object boundary remains valid"); + assert_eq!( + unknown.parse_host_manifest(NativeMessagingHostPlatform::Linux), + Err(NativeMessagingManifestParseError::UnknownField) + ); + + let missing_description = NativeMessagingManifestDocument::parse( + br#"{ + "name":"com.contextualwisdom.originweave", + "path":"/opt/originweave/native-host", + "type":"stdio", + "allowed_origins":["chrome-extension://abcdefghijklmnopabcdefghijklmnop/"] + }"#, + ) + .expect("outer object boundary remains valid"); + assert_eq!( + missing_description.parse_host_manifest(NativeMessagingHostPlatform::Linux), + Err(NativeMessagingManifestParseError::MissingRequiredField) + ); + + let wrong_type = NativeMessagingManifestDocument::parse( + br#"{ + "name":"com.contextualwisdom.originweave", + "description":"host", + "path":"/opt/originweave/native-host", + "type":"stdio", + "allowed_origins":"chrome-extension://abcdefghijklmnopabcdefghijklmnop/" + }"#, + ) + .expect("outer object boundary remains valid"); + assert_eq!( + wrong_type.parse_host_manifest(NativeMessagingHostPlatform::Linux), + Err(NativeMessagingManifestParseError::InvalidFieldType) + ); +} + +#[test] +fn native_messaging_manifest_document_preserves_typed_manifest_validation_failure() { + let document = NativeMessagingManifestDocument::parse( + br#"{ + "name":"com.contextualwisdom.originweave", + "description":"host", + "path":"/opt/originweave/native-host", + "type":"pipe", + "allowed_origins":["chrome-extension://abcdefghijklmnopabcdefghijklmnop/"] + }"#, + ) + .expect("outer object boundary remains valid"); + + let error = document + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect_err("unsupported Chrome interface type must fail closed"); + assert_eq!( + error, + NativeMessagingManifestParseError::Manifest( + NativeMessagingHostManifestError::UnsupportedInterfaceType + ) + ); + assert!(error.source().is_some()); +} + #[test] fn native_messaging_manifest_document_errors_are_standard_and_source_free() { for (error, expected) in [ From 67701d238f5e8f890dd930c5e5d276f6fc6ff33d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:35:14 -0700 Subject: [PATCH 29/64] test(core): format native manifest parser regression --- .../tests/native_messaging_manifest_document.rs | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_document.rs b/crates/originweave-core/tests/native_messaging_manifest_document.rs index a8adf913e..33b33f288 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_document.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_document.rs @@ -56,7 +56,8 @@ fn native_messaging_manifest_document_requires_outer_object_boundary() { } #[test] -fn native_messaging_manifest_document_parses_complete_authority_fields() -> Result<(), Box> { +fn native_messaging_manifest_document_parses_complete_authority_fields() +-> Result<(), Box> { let json = format!( r#"{{ "name": "com.contextualwisdom.originweave", @@ -70,7 +71,10 @@ fn native_messaging_manifest_document_parses_complete_authority_fields() -> Resu let document = NativeMessagingManifestDocument::parse(json.as_bytes())?; let manifest = document.parse_host_manifest(NativeMessagingHostPlatform::Linux)?; - assert_eq!(manifest.host_name().as_str(), "com.contextualwisdom.originweave"); + assert_eq!( + manifest.host_name().as_str(), + "com.contextualwisdom.originweave" + ); assert_eq!(manifest.platform(), NativeMessagingHostPlatform::Linux); assert_eq!(manifest.executable_path(), LINUX_HOST_PATH); assert_eq!(manifest.allowed_extension_count(), 1); @@ -94,7 +98,10 @@ fn native_messaging_manifest_document_decodes_json_string_escapes_before_validat let document = NativeMessagingManifestDocument::parse(json.as_bytes())?; let manifest = document.parse_host_manifest(NativeMessagingHostPlatform::Linux)?; - assert_eq!(manifest.host_name().as_str(), "com.contextualwisdom.originweave"); + assert_eq!( + manifest.host_name().as_str(), + "com.contextualwisdom.originweave" + ); assert_eq!(manifest.executable_path(), LINUX_HOST_PATH); assert!(!manifest.supports_native_initiated_connections()); Ok(()) From 212b0733dda64a56eb3b65c8ad922787562fcb0f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:37:45 -0700 Subject: [PATCH 30/64] feat(core): parse complete native host manifest documents --- .../src/native_messaging_manifest_document.rs | 416 +++++++++++++++++- 1 file changed, 405 insertions(+), 11 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index cfdd127a4..7f54f8d78 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -1,13 +1,18 @@ -//! Bounded pre-parser ingress for one Chrome native-messaging host manifest document. +//! Bounded parsing for one Chrome native-messaging host manifest document. //! -//! This module deliberately stops before JSON parsing. It bounds untrusted document bytes, -//! validates UTF-8, and requires one outer object-shaped envelope using only JSON whitespace -//! before a future manifest parser can allocate from or interpret structured fields. Admission -//! here does not prove that the document is valid JSON, installed by Chrome, authenticated by -//! the operating system, or safe to use as process or Agent authority. +//! This module bounds untrusted document bytes before allocation, validates complete JSON syntax +//! for the reviewed native-host schema, and then delegates authority-bearing field validation to +//! [`NativeMessagingHostManifest`]. Parsing a document does not prove that the manifest is +//! installed by Chrome, authenticated by the operating system, or safe to use as process or Agent +//! authority. use std::fmt; +use crate::{ + NativeMessagingHostManifest, NativeMessagingHostManifestError, NativeMessagingHostName, + NativeMessagingHostNameError, NativeMessagingHostPlatform, +}; + /// Maximum UTF-8 byte length accepted for one native-messaging host manifest document. /// /// Chrome does not define this OriginWeave-specific 64 KiB safety budget. The limit exists to @@ -18,21 +23,22 @@ pub const MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES: usize = 64 * 1024; /// /// Possessing this value proves only that the original byte document was non-empty, within the /// OriginWeave ingress budget, valid UTF-8, and wrapped by one outer object boundary after JSON -/// whitespace is ignored. It is not proof of valid JSON and is not a validated host manifest; -/// it carries no installation, origin, executable, process, or Agent authority. +/// whitespace is ignored. Call [`Self::parse_host_manifest`] to establish complete JSON/schema +/// validity and the existing host-manifest authority contract. This value alone carries no +/// installation, origin, executable, process, or Agent authority. #[derive(Debug, Clone, PartialEq, Eq)] pub struct NativeMessagingManifestDocument { text: String, } impl NativeMessagingManifestDocument { - /// Admit one untrusted manifest document before JSON parsing. + /// Admit one untrusted manifest document before structured JSON parsing. /// /// The byte-size check runs before UTF-8 decoding or allocation of the stored `String` so /// oversized input cannot force unbounded parser or text-storage work. Empty input, invalid /// UTF-8, and documents whose first and last non-JSON-whitespace characters are not `{` and - /// `}` fail closed. The object-envelope check is only a cheap ingress guard; a later - /// structured parser must still prove complete JSON syntax and field semantics. + /// `}` fail closed. The object-envelope check is only a cheap ingress guard; + /// [`Self::parse_host_manifest`] must still prove complete JSON syntax and field semantics. pub fn parse(bytes: &[u8]) -> Result { if bytes.is_empty() { return Err(NativeMessagingManifestDocumentError::EmptyDocument); @@ -51,6 +57,39 @@ impl NativeMessagingManifestDocument { }) } + /// Parse the complete reviewed Chrome native-host manifest schema and validate its authority. + /// + /// The parser accepts exactly the required `name`, `description`, `path`, `type`, and + /// `allowed_origins` members plus the optional boolean + /// `supports_native_initiated_connections`. Duplicate decoded member names, unknown members, + /// missing required members, wrong JSON types, malformed escapes, malformed arrays, trailing + /// commas, and trailing JSON data all fail closed. JSON strings are decoded before the + /// existing host-name, path, interface, and extension-origin validators run. + /// + /// `description` is required and type-checked because Chrome's manifest schema requires it, + /// but it is intentionally not retained as authority. The optional native-initiated field + /// defaults to `false` when absent. A successful result still does not prove installation, + /// filesystem ownership, executable identity, process provenance, feature/policy enablement, + /// message provenance, or Agent authority. + pub fn parse_host_manifest( + &self, + platform: NativeMessagingHostPlatform, + ) -> Result { + let fields = ManifestJsonParser::new(&self.text).parse_manifest()?; + let host_name = NativeMessagingHostName::parse(&fields.name) + .map_err(NativeMessagingManifestParseError::HostName)?; + let allowed_origins: Vec<&str> = fields.allowed_origins.iter().map(String::as_str).collect(); + NativeMessagingHostManifest::parse_with_native_initiated_connections( + host_name, + platform, + &fields.executable_path, + &fields.interface_type, + fields.supports_native_initiated_connections, + &allowed_origins, + ) + .map_err(NativeMessagingManifestParseError::Manifest) + } + /// Return the exact validated UTF-8 text without interpreting JSON fields. #[must_use] pub fn as_str(&self) -> &str { @@ -58,6 +97,307 @@ impl NativeMessagingManifestDocument { } } +#[derive(Debug, PartialEq, Eq)] +struct ManifestFields { + name: String, + executable_path: String, + interface_type: String, + allowed_origins: Vec, + supports_native_initiated_connections: bool, +} + +#[derive(Debug, Default)] +struct PartialManifestFields { + name: Option, + description: Option, + executable_path: Option, + interface_type: Option, + allowed_origins: Option>, + supports_native_initiated_connections: Option, +} + +impl PartialManifestFields { + fn finish(self) -> Result { + let (Some(name), Some(_description), Some(executable_path), Some(interface_type), Some(allowed_origins)) = ( + self.name, + self.description, + self.executable_path, + self.interface_type, + self.allowed_origins, + ) else { + return Err(NativeMessagingManifestParseError::MissingRequiredField); + }; + Ok(ManifestFields { + name, + executable_path, + interface_type, + allowed_origins, + supports_native_initiated_connections: self + .supports_native_initiated_connections + .unwrap_or(false), + }) + } +} + +struct ManifestJsonParser<'a> { + input: &'a str, + position: usize, +} + +impl<'a> ManifestJsonParser<'a> { + const fn new(input: &'a str) -> Self { + Self { input, position: 0 } + } + + fn parse_manifest(mut self) -> Result { + self.skip_whitespace(); + self.expect_byte(b'{')?; + self.skip_whitespace(); + let mut fields = PartialManifestFields::default(); + if self.peek_byte() == Some(b'}') { + self.position += 1; + } else { + loop { + let key = self.parse_string()?; + self.skip_whitespace(); + self.expect_byte(b':')?; + self.skip_whitespace(); + self.parse_field(&key, &mut fields)?; + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + if self.peek_byte() == Some(b'}') { + return Err(NativeMessagingManifestParseError::InvalidJson); + } + } + Some(b'}') => { + self.position += 1; + break; + } + _ => return Err(NativeMessagingManifestParseError::InvalidJson), + } + } + } + self.skip_whitespace(); + if self.position != self.input.len() { + return Err(NativeMessagingManifestParseError::InvalidJson); + } + fields.finish() + } + + fn parse_field( + &mut self, + key: &str, + fields: &mut PartialManifestFields, + ) -> Result<(), NativeMessagingManifestParseError> { + match key { + "name" => { + if fields.name.is_some() { + return Err(NativeMessagingManifestParseError::DuplicateField); + } + fields.name = Some(self.parse_typed_string()?); + } + "description" => { + if fields.description.is_some() { + return Err(NativeMessagingManifestParseError::DuplicateField); + } + fields.description = Some(self.parse_typed_string()?); + } + "path" => { + if fields.executable_path.is_some() { + return Err(NativeMessagingManifestParseError::DuplicateField); + } + fields.executable_path = Some(self.parse_typed_string()?); + } + "type" => { + if fields.interface_type.is_some() { + return Err(NativeMessagingManifestParseError::DuplicateField); + } + fields.interface_type = Some(self.parse_typed_string()?); + } + "allowed_origins" => { + if fields.allowed_origins.is_some() { + return Err(NativeMessagingManifestParseError::DuplicateField); + } + fields.allowed_origins = Some(self.parse_string_array()?); + } + "supports_native_initiated_connections" => { + if fields.supports_native_initiated_connections.is_some() { + return Err(NativeMessagingManifestParseError::DuplicateField); + } + fields.supports_native_initiated_connections = Some(self.parse_boolean()?); + } + _ => return Err(NativeMessagingManifestParseError::UnknownField), + } + Ok(()) + } + + fn parse_typed_string(&mut self) -> Result { + if self.peek_byte() != Some(b'"') { + return Err(NativeMessagingManifestParseError::InvalidFieldType); + } + self.parse_string() + } + + fn parse_string_array(&mut self) -> Result, NativeMessagingManifestParseError> { + if self.peek_byte() != Some(b'[') { + return Err(NativeMessagingManifestParseError::InvalidFieldType); + } + self.position += 1; + self.skip_whitespace(); + let mut values = Vec::new(); + if self.peek_byte() == Some(b']') { + self.position += 1; + return Ok(values); + } + loop { + if self.peek_byte() != Some(b'"') { + return Err(NativeMessagingManifestParseError::InvalidFieldType); + } + values.push(self.parse_string()?); + self.skip_whitespace(); + match self.peek_byte() { + Some(b',') => { + self.position += 1; + self.skip_whitespace(); + if self.peek_byte() == Some(b']') { + return Err(NativeMessagingManifestParseError::InvalidJson); + } + } + Some(b']') => { + self.position += 1; + return Ok(values); + } + _ => return Err(NativeMessagingManifestParseError::InvalidJson), + } + } + } + + fn parse_boolean(&mut self) -> Result { + if self.input[self.position..].starts_with("true") { + self.position += 4; + return Ok(true); + } + if self.input[self.position..].starts_with("false") { + self.position += 5; + return Ok(false); + } + Err(NativeMessagingManifestParseError::InvalidFieldType) + } + + fn parse_string(&mut self) -> Result { + self.expect_byte(b'"')?; + let mut output = String::new(); + loop { + let Some(byte) = self.peek_byte() else { + return Err(NativeMessagingManifestParseError::InvalidJson); + }; + match byte { + b'"' => { + self.position += 1; + return Ok(output); + } + b'\\' => { + self.position += 1; + self.parse_escape(&mut output)?; + } + 0x00..=0x1f => return Err(NativeMessagingManifestParseError::InvalidJson), + _ => { + let Some(character) = self.input[self.position..].chars().next() else { + return Err(NativeMessagingManifestParseError::InvalidJson); + }; + output.push(character); + self.position += character.len_utf8(); + } + } + } + } + + fn parse_escape( + &mut self, + output: &mut String, + ) -> Result<(), NativeMessagingManifestParseError> { + let Some(escape) = self.take_byte() else { + return Err(NativeMessagingManifestParseError::InvalidJson); + }; + match escape { + b'"' => output.push('"'), + b'\\' => output.push('\\'), + b'/' => output.push('/'), + b'b' => output.push('\u{0008}'), + b'f' => output.push('\u{000c}'), + b'n' => output.push('\n'), + b'r' => output.push('\r'), + b't' => output.push('\t'), + b'u' => output.push(self.parse_unicode_escape()?), + _ => return Err(NativeMessagingManifestParseError::InvalidJson), + } + Ok(()) + } + + fn parse_unicode_escape(&mut self) -> Result { + let first = self.parse_hex_quad()?; + let scalar = if (0xd800..=0xdbff).contains(&first) { + if self.take_byte() != Some(b'\\') || self.take_byte() != Some(b'u') { + return Err(NativeMessagingManifestParseError::InvalidJson); + } + let second = self.parse_hex_quad()?; + if !(0xdc00..=0xdfff).contains(&second) { + return Err(NativeMessagingManifestParseError::InvalidJson); + } + 0x1_0000 + ((u32::from(first) - 0xd800) << 10) + (u32::from(second) - 0xdc00) + } else { + if (0xdc00..=0xdfff).contains(&first) { + return Err(NativeMessagingManifestParseError::InvalidJson); + } + u32::from(first) + }; + char::from_u32(scalar).ok_or(NativeMessagingManifestParseError::InvalidJson) + } + + fn parse_hex_quad(&mut self) -> Result { + if self.position + 4 > self.input.len() { + return Err(NativeMessagingManifestParseError::InvalidJson); + } + let mut value = 0_u16; + for _ in 0..4 { + let byte = self.input.as_bytes()[self.position]; + let Some(digit) = (byte as char).to_digit(16) else { + return Err(NativeMessagingManifestParseError::InvalidJson); + }; + value = (value << 4) | digit as u16; + self.position += 1; + } + Ok(value) + } + + fn skip_whitespace(&mut self) { + while matches!(self.peek_byte(), Some(b' ' | b'\t' | b'\r' | b'\n')) { + self.position += 1; + } + } + + fn expect_byte(&mut self, expected: u8) -> Result<(), NativeMessagingManifestParseError> { + if self.take_byte() == Some(expected) { + Ok(()) + } else { + Err(NativeMessagingManifestParseError::InvalidJson) + } + } + + fn peek_byte(&self) -> Option { + self.input.as_bytes().get(self.position).copied() + } + + fn take_byte(&mut self) -> Option { + let byte = self.peek_byte()?; + self.position += 1; + Some(byte) + } +} + /// Failure to admit a native-messaging host manifest document at the pre-parser boundary. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NativeMessagingManifestDocumentError { @@ -91,3 +431,57 @@ impl fmt::Display for NativeMessagingManifestDocumentError { } impl std::error::Error for NativeMessagingManifestDocumentError {} + +/// Failure to parse or validate a complete bounded native-messaging host manifest document. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum NativeMessagingManifestParseError { + /// The document was not one complete valid JSON object in the reviewed schema. + InvalidJson, + /// A decoded manifest member appeared more than once. + DuplicateField, + /// The manifest contained a member outside the reviewed Chrome native-host schema. + UnknownField, + /// One or more Chrome-required manifest members were absent. + MissingRequiredField, + /// A reviewed member used a JSON type different from the Chrome manifest contract. + InvalidFieldType, + /// The decoded host-name string violated the existing exact host-identity contract. + HostName(NativeMessagingHostNameError), + /// The decoded authority-bearing fields failed the existing host-manifest validator. + Manifest(NativeMessagingHostManifestError), +} + +impl fmt::Display for NativeMessagingManifestParseError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidJson => formatter.write_str("native messaging host manifest JSON is invalid"), + Self::DuplicateField => { + formatter.write_str("native messaging host manifest contains a duplicate field") + } + Self::UnknownField => { + formatter.write_str("native messaging host manifest contains an unknown field") + } + Self::MissingRequiredField => formatter + .write_str("native messaging host manifest is missing a required field"), + Self::InvalidFieldType => { + formatter.write_str("native messaging host manifest field has an invalid JSON type") + } + Self::HostName(error) => write!(formatter, "invalid native messaging host name: {error}"), + Self::Manifest(error) => write!(formatter, "invalid native messaging host manifest: {error}"), + } + } +} + +impl std::error::Error for NativeMessagingManifestParseError { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + match self { + Self::HostName(error) => Some(error), + Self::Manifest(error) => Some(error), + Self::InvalidJson + | Self::DuplicateField + | Self::UnknownField + | Self::MissingRequiredField + | Self::InvalidFieldType => None, + } + } +} From b8c239d8bb7ba6e0a3018c318fcb0e7e17329f4e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:38:01 -0700 Subject: [PATCH 31/64] feat(core): export native manifest parser errors --- crates/originweave-core/src/root.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs index fafe28e36..8aeb776d4 100644 --- a/crates/originweave-core/src/root.rs +++ b/crates/originweave-core/src/root.rs @@ -35,5 +35,5 @@ pub use native_messaging_manifest::{ mod native_messaging_manifest_document; pub use native_messaging_manifest_document::{ MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES, NativeMessagingManifestDocument, - NativeMessagingManifestDocumentError, + NativeMessagingManifestDocumentError, NativeMessagingManifestParseError, }; From a99a0d026abe46eb8ea5906d0e75690e2f60635c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 00:40:15 -0700 Subject: [PATCH 32/64] style(core): format native manifest parser --- .../src/native_messaging_manifest_document.rs | 31 ++++++++++++++----- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index 7f54f8d78..c65624bc8 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -78,7 +78,8 @@ impl NativeMessagingManifestDocument { let fields = ManifestJsonParser::new(&self.text).parse_manifest()?; let host_name = NativeMessagingHostName::parse(&fields.name) .map_err(NativeMessagingManifestParseError::HostName)?; - let allowed_origins: Vec<&str> = fields.allowed_origins.iter().map(String::as_str).collect(); + let allowed_origins: Vec<&str> = + fields.allowed_origins.iter().map(String::as_str).collect(); NativeMessagingHostManifest::parse_with_native_initiated_connections( host_name, platform, @@ -118,13 +119,20 @@ struct PartialManifestFields { impl PartialManifestFields { fn finish(self) -> Result { - let (Some(name), Some(_description), Some(executable_path), Some(interface_type), Some(allowed_origins)) = ( + let ( + Some(name), + Some(_description), + Some(executable_path), + Some(interface_type), + Some(allowed_origins), + ) = ( self.name, self.description, self.executable_path, self.interface_type, self.allowed_origins, - ) else { + ) + else { return Err(NativeMessagingManifestParseError::MissingRequiredField); }; Ok(ManifestFields { @@ -454,20 +462,27 @@ pub enum NativeMessagingManifestParseError { impl fmt::Display for NativeMessagingManifestParseError { fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { match self { - Self::InvalidJson => formatter.write_str("native messaging host manifest JSON is invalid"), + Self::InvalidJson => { + formatter.write_str("native messaging host manifest JSON is invalid") + } Self::DuplicateField => { formatter.write_str("native messaging host manifest contains a duplicate field") } Self::UnknownField => { formatter.write_str("native messaging host manifest contains an unknown field") } - Self::MissingRequiredField => formatter - .write_str("native messaging host manifest is missing a required field"), + Self::MissingRequiredField => { + formatter.write_str("native messaging host manifest is missing a required field") + } Self::InvalidFieldType => { formatter.write_str("native messaging host manifest field has an invalid JSON type") } - Self::HostName(error) => write!(formatter, "invalid native messaging host name: {error}"), - Self::Manifest(error) => write!(formatter, "invalid native messaging host manifest: {error}"), + Self::HostName(error) => { + write!(formatter, "invalid native messaging host name: {error}") + } + Self::Manifest(error) => { + write!(formatter, "invalid native messaging host manifest: {error}") + } } } } From b4d583e3c43878f9f06d4f1d431ecffde4943a62 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 03:17:55 -0700 Subject: [PATCH 33/64] test(core): cover native manifest parser failure edges --- ...ative_messaging_manifest_document_edges.rs | 177 ++++++++++++++++++ 1 file changed, 177 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_manifest_document_edges.rs diff --git a/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs b/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs new file mode 100644 index 000000000..983dda0d3 --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs @@ -0,0 +1,177 @@ +#![allow(clippy::expect_used)] + +use std::error::Error; + +use originweave_core::{ + NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, +}; + +const EXTENSION_ORIGIN: &str = + "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"; + +fn parse_error(raw: &str) -> NativeMessagingManifestParseError { + NativeMessagingManifestDocument::parse(raw.as_bytes()) + .expect("edge fixture must pass only the bounded outer-object pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect_err("edge fixture must fail complete manifest parsing or authority validation") +} + +#[test] +fn complete_parser_rejects_every_duplicate_reviewed_field() { + let cases = [ + format!( + r#"{{"name":"com.contextualwisdom.originweave","name":"com.contextualwisdom.other","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","description":"other","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","path":"/tmp/other","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":true,"supports_native_initiated_connections":false}}"# + ), + ]; + + for raw in cases { + assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::DuplicateField); + } +} + +#[test] +fn complete_parser_covers_empty_and_malformed_array_and_boolean_shapes() { + let empty_object = NativeMessagingManifestDocument::parse(b"{}") + .expect("empty object passes only the bounded outer-object pre-parser"); + assert_eq!( + empty_object.parse_host_manifest(NativeMessagingHostPlatform::Linux), + Err(NativeMessagingManifestParseError::MissingRequiredField) + ); + + let empty_origins = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[]}}"# + ); + assert!(matches!( + parse_error(&empty_origins), + NativeMessagingManifestParseError::Manifest(_) + )); + + for raw in [ + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[true]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":"{EXTENSION_ORIGIN}"}}"# + ), + ] { + assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidFieldType); + } + + for raw in [ + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}",]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}" "{EXTENSION_ORIGIN}"]}}"# + ), + "{} {}".to_owned(), + ] { + assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidJson); + } + + let invalid_boolean = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":1}}"# + ); + assert_eq!( + parse_error(&invalid_boolean), + NativeMessagingManifestParseError::InvalidFieldType + ); + + let false_boolean = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":false}}"# + ); + let manifest = NativeMessagingManifestDocument::parse(false_boolean.as_bytes()) + .expect("valid false-boolean fixture passes pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect("valid false-boolean fixture passes complete parsing"); + assert!(!manifest.supports_native_initiated_connections()); +} + +#[test] +fn complete_parser_covers_json_escape_and_unicode_failure_edges() { + let escaped_description = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"a\b\f\n\r\t-\u0041-\u00E9-\u263A-\uD83D\uDE00-\u00AF","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ); + let manifest = NativeMessagingManifestDocument::parse(escaped_description.as_bytes()) + .expect("valid escaped-string fixture passes pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect("valid escaped-string fixture passes complete parsing"); + assert_eq!(manifest.allowed_extension_count(), 1); + + for description in [ + r#"bad\q"#, + r#"bad\uD83D"#, + r#"bad\uD83D\x0000"#, + r#"bad\uD83D\u0041"#, + r#"bad\uDE00"#, + r#"bad\u12"#, + r#"bad\u00G0"#, + ] { + let raw = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"{description}","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ); + assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidJson); + } + + let raw_control = format!( + "{{\"name\":\"com.contextualwisdom.originweave\",\"description\":\"bad\u{0001}text\",\"path\":\"/opt/originweave/native-host\",\"type\":\"stdio\",\"allowed_origins\":[\"{EXTENSION_ORIGIN}\"]}}" + ); + assert_eq!( + parse_error(&raw_control), + NativeMessagingManifestParseError::InvalidJson + ); + + let unterminated = r#"{"name":"unterminated}"#; + assert_eq!( + parse_error(unterminated), + NativeMessagingManifestParseError::InvalidJson + ); +} + +#[test] +fn parse_errors_expose_deterministic_display_and_only_causal_sources() { + for error in [ + NativeMessagingManifestParseError::InvalidJson, + NativeMessagingManifestParseError::DuplicateField, + NativeMessagingManifestParseError::UnknownField, + NativeMessagingManifestParseError::MissingRequiredField, + NativeMessagingManifestParseError::InvalidFieldType, + ] { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_none()); + } + + let invalid_host = format!( + r#"{{"name":"INVALID HOST","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ); + let host_error = parse_error(&invalid_host); + assert!(matches!(host_error, NativeMessagingManifestParseError::HostName(_))); + assert!(!host_error.to_string().is_empty()); + assert!(host_error.source().is_some()); + + let invalid_manifest = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"pipe","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ); + let manifest_error = parse_error(&invalid_manifest); + assert!(matches!( + manifest_error, + NativeMessagingManifestParseError::Manifest(_) + )); + assert!(!manifest_error.to_string().is_empty()); + assert!(manifest_error.source().is_some()); +} From 72c0014f0b3e5dad0007a53ae5e2fb3009c96100 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:23:45 -0700 Subject: [PATCH 34/64] test(extension): close manifest parser coverage gaps --- .../src/native_messaging_manifest_document.rs | 89 +++++++++++++++---- 1 file changed, 72 insertions(+), 17 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index c65624bc8..e7e29e881 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -152,6 +152,10 @@ struct ManifestJsonParser<'a> { position: usize, } +fn decoded_json_string(bytes: Vec) -> Result { + String::from_utf8(bytes).map_err(|_error| NativeMessagingManifestParseError::InvalidJson) +} + impl<'a> ManifestJsonParser<'a> { const fn new(input: &'a str) -> Self { Self { input, position: 0 } @@ -297,7 +301,7 @@ impl<'a> ManifestJsonParser<'a> { fn parse_string(&mut self) -> Result { self.expect_byte(b'"')?; - let mut output = String::new(); + let mut output = Vec::new(); loop { let Some(byte) = self.peek_byte() else { return Err(NativeMessagingManifestParseError::InvalidJson); @@ -305,7 +309,7 @@ impl<'a> ManifestJsonParser<'a> { match byte { b'"' => { self.position += 1; - return Ok(output); + return decoded_json_string(output); } b'\\' => { self.position += 1; @@ -313,11 +317,8 @@ impl<'a> ManifestJsonParser<'a> { } 0x00..=0x1f => return Err(NativeMessagingManifestParseError::InvalidJson), _ => { - let Some(character) = self.input[self.position..].chars().next() else { - return Err(NativeMessagingManifestParseError::InvalidJson); - }; - output.push(character); - self.position += character.len_utf8(); + output.push(byte); + self.position += 1; } } } @@ -325,21 +326,25 @@ impl<'a> ManifestJsonParser<'a> { fn parse_escape( &mut self, - output: &mut String, + output: &mut Vec, ) -> Result<(), NativeMessagingManifestParseError> { let Some(escape) = self.take_byte() else { return Err(NativeMessagingManifestParseError::InvalidJson); }; match escape { - b'"' => output.push('"'), - b'\\' => output.push('\\'), - b'/' => output.push('/'), - b'b' => output.push('\u{0008}'), - b'f' => output.push('\u{000c}'), - b'n' => output.push('\n'), - b'r' => output.push('\r'), - b't' => output.push('\t'), - b'u' => output.push(self.parse_unicode_escape()?), + b'"' => output.push(b'"'), + b'\\' => output.push(b'\\'), + b'/' => output.push(b'/'), + b'b' => output.push(0x08), + b'f' => output.push(0x0c), + b'n' => output.push(b'\n'), + b'r' => output.push(b'\r'), + b't' => output.push(b'\t'), + b'u' => { + let character = self.parse_unicode_escape()?; + let mut encoded = [0_u8; 4]; + output.extend_from_slice(character.encode_utf8(&mut encoded).as_bytes()); + } _ => return Err(NativeMessagingManifestParseError::InvalidJson), } Ok(()) @@ -500,3 +505,53 @@ impl std::error::Error for NativeMessagingManifestParseError { } } } + +#[cfg(test)] +mod tests { + use super::*; + + const INVALID_JSON: NativeMessagingManifestParseError = + NativeMessagingManifestParseError::InvalidJson; + + #[test] + fn parser_propagates_structural_and_typed_field_failures() { + for raw in [ + "[]", + "{?}", + r#"{"name" "value"}"#, + r#"{"path":"\q"}"#, + r#"{"type":"\q"}"#, + ] { + assert_eq!(ManifestJsonParser::new(raw).parse_manifest(), Err(INVALID_JSON)); + } + + assert_eq!( + ManifestJsonParser::new(r#"{"name":1}"#).parse_manifest(), + Err(NativeMessagingManifestParseError::InvalidFieldType) + ); + } + + #[test] + fn parser_propagates_array_escape_unicode_and_byte_boundary_failures() { + assert_eq!( + ManifestJsonParser::new(r#"{"allowed_origins":["\q"]}"#).parse_manifest(), + Err(INVALID_JSON) + ); + assert_eq!( + ManifestJsonParser::new(r#"{"allowed_origins":["origin",]}"#).parse_manifest(), + Err(INVALID_JSON) + ); + + let mut empty_escape = ManifestJsonParser::new(""); + let mut output = Vec::new(); + assert_eq!(empty_escape.parse_escape(&mut output), Err(INVALID_JSON)); + + let mut short_quad = ManifestJsonParser::new("12"); + assert_eq!(short_quad.parse_hex_quad(), Err(INVALID_JSON)); + + let mut short_second_quad = ManifestJsonParser::new("D83D\\u12"); + assert_eq!(short_second_quad.parse_unicode_escape(), Err(INVALID_JSON)); + + assert_eq!(decoded_json_string(vec![0xff]), Err(INVALID_JSON)); + } +} From 8c000516cc152b66aed8eac085efc27565dda35b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:25:38 -0700 Subject: [PATCH 35/64] style(extension): apply canonical manifest edge formatting --- ...ative_messaging_manifest_document_edges.rs | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs b/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs index 983dda0d3..f5bcc5eae 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs @@ -6,8 +6,7 @@ use originweave_core::{ NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, }; -const EXTENSION_ORIGIN: &str = - "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"; +const EXTENSION_ORIGIN: &str = "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"; fn parse_error(raw: &str) -> NativeMessagingManifestParseError { NativeMessagingManifestDocument::parse(raw.as_bytes()) @@ -40,7 +39,10 @@ fn complete_parser_rejects_every_duplicate_reviewed_field() { ]; for raw in cases { - assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::DuplicateField); + assert_eq!( + parse_error(&raw), + NativeMessagingManifestParseError::DuplicateField + ); } } @@ -69,7 +71,10 @@ fn complete_parser_covers_empty_and_malformed_array_and_boolean_shapes() { r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":"{EXTENSION_ORIGIN}"}}"# ), ] { - assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidFieldType); + assert_eq!( + parse_error(&raw), + NativeMessagingManifestParseError::InvalidFieldType + ); } for raw in [ @@ -81,7 +86,10 @@ fn complete_parser_covers_empty_and_malformed_array_and_boolean_shapes() { ), "{} {}".to_owned(), ] { - assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidJson); + assert_eq!( + parse_error(&raw), + NativeMessagingManifestParseError::InvalidJson + ); } let invalid_boolean = format!( @@ -125,7 +133,10 @@ fn complete_parser_covers_json_escape_and_unicode_failure_edges() { let raw = format!( r#"{{"name":"com.contextualwisdom.originweave","description":"{description}","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# ); - assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidJson); + assert_eq!( + parse_error(&raw), + NativeMessagingManifestParseError::InvalidJson + ); } let raw_control = format!( @@ -160,7 +171,10 @@ fn parse_errors_expose_deterministic_display_and_only_causal_sources() { r#"{{"name":"INVALID HOST","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# ); let host_error = parse_error(&invalid_host); - assert!(matches!(host_error, NativeMessagingManifestParseError::HostName(_))); + assert!(matches!( + host_error, + NativeMessagingManifestParseError::HostName(_) + )); assert!(!host_error.to_string().is_empty()); assert!(host_error.source().is_some()); From 75b4da333f9fe63681e8efa31797d4a84e846e2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 09:20:49 -0700 Subject: [PATCH 36/64] test(extension): close manifest parser coverage gaps --- .../src/native_messaging_manifest_document.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index e7e29e881..3f592539a 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -522,7 +522,10 @@ mod tests { r#"{"path":"\q"}"#, r#"{"type":"\q"}"#, ] { - assert_eq!(ManifestJsonParser::new(raw).parse_manifest(), Err(INVALID_JSON)); + assert_eq!( + ManifestJsonParser::new(raw).parse_manifest(), + Err(INVALID_JSON) + ); } assert_eq!( @@ -542,6 +545,9 @@ mod tests { Err(INVALID_JSON) ); + let mut trailing_array = ManifestJsonParser::new(r#"["origin",]"#); + assert_eq!(trailing_array.parse_string_array(), Err(INVALID_JSON)); + let mut empty_escape = ManifestJsonParser::new(""); let mut output = Vec::new(); assert_eq!(empty_escape.parse_escape(&mut output), Err(INVALID_JSON)); From 075612d526ae8ce97d2e5f486cd12993c37512d4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 16:14:23 -0700 Subject: [PATCH 37/64] test(extension): close parser CI gaps --- ...native_messaging_manifest_document_edges.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs b/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs index f5bcc5eae..ba05b983c 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs @@ -7,6 +7,7 @@ use originweave_core::{ }; const EXTENSION_ORIGIN: &str = "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"; +const SECOND_EXTENSION_ORIGIN: &str = "chrome-extension://ponmlkjihgfedcbaponmlkjihgfedcba/"; fn parse_error(raw: &str) -> NativeMessagingManifestParseError { NativeMessagingManifestDocument::parse(raw.as_bytes()) @@ -55,18 +56,23 @@ fn complete_parser_covers_empty_and_malformed_array_and_boolean_shapes() { Err(NativeMessagingManifestParseError::MissingRequiredField) ); - let empty_origins = format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[]}}"# - ); + let empty_origins = r#"{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[]}"#.to_owned(); assert!(matches!( parse_error(&empty_origins), NativeMessagingManifestParseError::Manifest(_) )); + let multiple_origins = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}","{SECOND_EXTENSION_ORIGIN}"]}}"# + ); + let multiple_manifest = NativeMessagingManifestDocument::parse(multiple_origins.as_bytes()) + .expect("valid multi-origin fixture passes pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect("valid multi-origin fixture passes complete parsing"); + assert_eq!(multiple_manifest.allowed_extension_count(), 2); + for raw in [ - format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[true]}}"# - ), + r#"{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[true]}"#.to_owned(), format!( r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":"{EXTENSION_ORIGIN}"}}"# ), From 934f16e80c782a17f35e92c115b54693d95d4754 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 19:08:05 -0700 Subject: [PATCH 38/64] test(extension): cover malformed manifest separator --- .../native_messaging_manifest_syntax_boundary.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs diff --git a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs new file mode 100644 index 000000000..695bddad4 --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs @@ -0,0 +1,16 @@ +#![allow(clippy::expect_used)] + +use originweave_core::{ + NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, +}; + +#[test] +fn complete_parser_rejects_a_missing_member_separator_after_bounded_admission() { + let document = NativeMessagingManifestDocument::parse(br#"{"name" "value"}"#) + .expect("malformed object fixture must pass only the bounded outer-object pre-parser"); + + assert!(matches!( + document.parse_host_manifest(NativeMessagingHostPlatform::Linux), + Err(NativeMessagingManifestParseError::InvalidJson) + )); +} From 1e0d72428c8d0d8792deb33fdd3b4a0c889ee0bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 19:09:46 -0700 Subject: [PATCH 39/64] docs(extension): align manifest parser traceability --- .../extension-authority-security.md | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/docs/traceability/extension-authority-security.md b/docs/traceability/extension-authority-security.md index 3cb1e8bf2..8dc779457 100644 --- a/docs/traceability/extension-authority-security.md +++ b/docs/traceability/extension-authority-security.md @@ -68,13 +68,15 @@ Exact current head `c639cd78e3acad235be4cbbfdef67b84ce7ddbfa` provides bounded n Exact current head `4a71b7dd357974f791f8d7f4a0be5c4c0b9ea1b1`, stacked on #82, preserves current extension authority while bounding native-endian message framing with direction-specific payload ceilings, exact frame length, and UTF-8 text validation before later JSON/untrusted-observation handling. It does not prove host-manifest installation, process ownership, sandboxing, stdio provenance, or Agent authority. -### PR #169 — validated host-manifest authority +### PR #169 — complete bounded host-manifest parsing and authority validation **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -This Draft, stacked on #154, adds test-first host-manifest authority. Before future structured parsing, the raw manifest document must be non-empty valid UTF-8, at most the OriginWeave 64 KiB pre-parser safety budget, and wrapped by one outer object-shaped envelope after the four JSON whitespace characters are ignored; the byte bound is enforced before storing the document as a `String`. The envelope check is deliberately only an ingress guard: it does not establish complete JSON syntax or field validity. The budget is an OriginWeave resource-governance limit, not a Chrome or operating-system manifest-size claim, and successful admission does not establish authority. The structured contract accepts only exact `stdio`, requires a non-empty bounded raw `allowed_origins` list, validates exact canonical `chrome-extension:///` origins without wildcard or suffix normalization, collapses duplicate exact origins without widening authority, bounds executable-path allocation, preserves platform-specific path semantics, allows only an exact host plus explicitly listed extension identity, and retains the optional boolean `supports_native_initiated_connections` declaration when a trusted structured parser supplies it. The compatibility constructor records that declaration as absent/false rather than inventing support. +This Draft, stacked on #154, now parses the admitted native-host manifest document completely at the reviewed schema boundary. Raw ingress must first be non-empty valid UTF-8, remain within the OriginWeave 64 KiB pre-parser safety budget before `String` allocation, and have one outer object-shaped envelope after the four JSON whitespace characters are ignored. Complete parsing then accepts exactly Chrome's reviewed required `name`, `description`, `path`, `type`, and `allowed_origins` members plus optional boolean `supports_native_initiated_connections`; rejects duplicate decoded member names, unknown or missing members, wrong JSON types, malformed escapes and surrogate pairs, malformed arrays, trailing commas, control characters, and trailing JSON data; and decodes JSON strings before authority-bearing values reach the existing validators. -The implementation intentionally treats bounded document admission and caller-supplied validated manifest fields as separate preconditions rather than ambient authority. A retained `supports_native_initiated_connections: true` value proves only that the validated manifest declared the optional Chromium field; it does not prove Chromium feature or enterprise-policy enablement, installed registration, executable/process identity, native-initiated connection provenance, message trust, or Agent authority. This slice still does not completely parse or validate JSON from the admitted document, read or authenticate filesystem/registry registration, canonicalize or attest executable paths, resolve a Windows relative path against an authenticated manifest directory, prove installer/OS ownership, spawn/sandbox/supervise a host process, authenticate the stdio peer, parse/trust host JSON, expose protected values, or grant Agent actions. Those remain separately reviewed runtime boundaries. +`description` is required and type-checked because it belongs to the reviewed manifest schema but is intentionally not retained as authority. The parsed contract accepts only exact `stdio`, requires a non-empty bounded raw `allowed_origins` list, validates exact canonical `chrome-extension:///` origins without wildcard or suffix normalization, collapses duplicate exact origins without widening authority, bounds executable-path allocation, preserves platform-specific path semantics, allows only an exact host plus explicitly listed extension identity, and retains the optional `supports_native_initiated_connections` declaration as data. The raw-document and executable-path limits are OriginWeave resource-governance limits, not Chrome or operating-system maximum claims. + +Successful parsing still does not prove that the manifest is installed, authentic, OS-owned, policy-enabled, or bound to the process that will exchange native messages. A retained `supports_native_initiated_connections: true` value proves only that the parsed document declared the reviewed optional field; it does not prove Chromium feature or enterprise-policy enablement, installed registration, executable/process identity, native-initiated connection provenance, message trust, or Agent authority. This slice still does not read or authenticate filesystem/registry registration, canonicalize or attest an installed executable path, resolve a Windows relative path against an authenticated manifest directory, prove installer/OS ownership, spawn/sandbox/supervise a host process, authenticate the stdio peer, trust host message semantics, expose protected values, or grant Agent actions. Those remain separately reviewed runtime boundaries. ## 4. Security interpretation @@ -98,25 +100,26 @@ and: Chrome nativeMessaging permission -> exact extension/host grant -> bounded UTF-8, object-shaped manifest-document ingress --> validated exact host-manifest allow-list +-> complete bounded reviewed JSON/schema parsing +-> validated exact host-manifest allow-list and executable-path intent -> optional native-initiated-connection declaration retained as data only -> bounded native-messaging framing -/> Chromium feature / enterprise-policy enablement --/> complete JSON validity / installed-host ownership +-/> installed-host ownership / executable-process identity -/> process identity / sandbox authority -/> trusted message provenance -/> Agent authority -/> protected-value access ``` -A future real extension/native-host adapter must preserve these separations. Chrome permissions, extension grants, bounded document bytes, validated host-manifest fields, optional connection-direction declarations, and framed native bytes are inputs to explicit policy/provenance composition, never ambient authority that bypasses deterministic Agent or sensitive-data controls. +A future real extension/native-host adapter must preserve these separations. Chrome permissions, extension grants, bounded and fully parsed manifest documents, validated host-manifest fields, optional connection-direction declarations, and framed native bytes are inputs to explicit policy/provenance composition, never ambient authority that bypasses deterministic Agent or sensitive-data controls. ## 5. Remaining issue #27 / #10 boundary This dossier does **not** close issue #27 or issue #10. Remaining material work includes, among other accepted requirements: -- complete structured JSON parsing with bounded field extraction plus trusted platform-specific native-host registration discovery and ownership/path validation; -- process sandboxing, lifecycle supervision, authenticated stdio peer attribution, crash recovery, and untrusted-message handling; +- trusted platform-specific native-host registration discovery, ownership checks, and installed-path validation, including Windows relative-path resolution against an authenticated manifest directory; +- process sandboxing, lifecycle supervision, authenticated stdio peer attribution, crash recovery, and fail-closed untrusted-message handling; - real managed-extension allow-list and enterprise policy integration, including independent validation of any Chromium native-initiated-connection feature/policy state before use; - complete supported-capability release matrix and regression gate; - authenticated workload/service identity for sensitive-data broker audience; From 3d8821b0dee387a0ac09e3e47e6eaf1b8ec96719 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 19:17:31 -0700 Subject: [PATCH 40/64] test(extension): exercise public parser rejection edges --- ...tive_messaging_manifest_syntax_boundary.rs | 32 +++++++++++++++---- 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs index 695bddad4..5fd3e74ab 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs @@ -4,13 +4,33 @@ use originweave_core::{ NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, }; +fn parse_error(bytes: &[u8]) -> NativeMessagingManifestParseError { + NativeMessagingManifestDocument::parse(bytes) + .expect("malformed object fixture must pass only the bounded outer-object pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect_err("malformed manifest fixture must fail complete parsing") +} + #[test] fn complete_parser_rejects_a_missing_member_separator_after_bounded_admission() { - let document = NativeMessagingManifestDocument::parse(br#"{"name" "value"}"#) - .expect("malformed object fixture must pass only the bounded outer-object pre-parser"); + assert_eq!( + parse_error(br#"{"name" "value"}"#), + NativeMessagingManifestParseError::InvalidJson + ); +} - assert!(matches!( - document.parse_host_manifest(NativeMessagingHostPlatform::Linux), - Err(NativeMessagingManifestParseError::InvalidJson) - )); +#[test] +fn complete_parser_rejects_non_string_required_fields_through_the_public_boundary() { + assert_eq!( + parse_error(br#"{"name":true}"#), + NativeMessagingManifestParseError::InvalidFieldType + ); +} + +#[test] +fn complete_parser_rejects_a_unicode_escape_truncated_by_the_outer_object_boundary() { + assert_eq!( + parse_error(br#"{"name":"\u1"}"#), + NativeMessagingManifestParseError::InvalidJson + ); } From 3878d6bc02b17ccad4ed04f6e549d129e37cf03f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:14:08 -0700 Subject: [PATCH 41/64] test(extension): close native manifest parser coverage edges --- .../native_messaging_manifest_syntax_boundary.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs index 5fd3e74ab..7e0527e99 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs @@ -34,3 +34,16 @@ fn complete_parser_rejects_a_unicode_escape_truncated_by_the_outer_object_bounda NativeMessagingManifestParseError::InvalidJson ); } + +#[test] +fn complete_parser_preserves_nested_string_failures_at_each_schema_boundary() { + for raw in [ + br#"{"\q":"value"}"#.as_slice(), + br#"{"path":"\q"}"#.as_slice(), + br#"{"type":"\q"}"#.as_slice(), + br#"{"allowed_origins":["\q"]}"#.as_slice(), + br#"{"name":"\uD83D\u12"}"#.as_slice(), + ] { + assert_eq!(parse_error(raw), NativeMessagingManifestParseError::InvalidJson); + } +} From 419e88b8952405f12638aeb6a2b671729557e9ee Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 20:15:45 -0700 Subject: [PATCH 42/64] style(extension): apply canonical rustfmt --- .../tests/native_messaging_manifest_syntax_boundary.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs index 7e0527e99..b5b6b94f0 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs @@ -44,6 +44,9 @@ fn complete_parser_preserves_nested_string_failures_at_each_schema_boundary() { br#"{"allowed_origins":["\q"]}"#.as_slice(), br#"{"name":"\uD83D\u12"}"#.as_slice(), ] { - assert_eq!(parse_error(raw), NativeMessagingManifestParseError::InvalidJson); + assert_eq!( + parse_error(raw), + NativeMessagingManifestParseError::InvalidJson + ); } } From e9da4dd945f957b27ebf2a3a73cf9f478e0d933b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:44:02 -0700 Subject: [PATCH 43/64] test(extension): cover malformed manifest member key --- .../tests/native_messaging_manifest_syntax_boundary.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs index b5b6b94f0..4fd4d012a 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs @@ -19,6 +19,14 @@ fn complete_parser_rejects_a_missing_member_separator_after_bounded_admission() ); } +#[test] +fn complete_parser_rejects_a_non_string_member_key_after_bounded_admission() { + assert_eq!( + parse_error(br#"{?}"#), + NativeMessagingManifestParseError::InvalidJson + ); +} + #[test] fn complete_parser_rejects_non_string_required_fields_through_the_public_boundary() { assert_eq!( From d28e72a8b7c83ee3fac8afa29044dfde10a93024 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 22:55:32 -0700 Subject: [PATCH 44/64] refactor(extension): encode manifest envelope invariant --- .../src/native_messaging_manifest_document.rs | 57 ++++++++++++------- 1 file changed, 38 insertions(+), 19 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index 3f592539a..f64c9b278 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -75,7 +75,7 @@ impl NativeMessagingManifestDocument { &self, platform: NativeMessagingHostPlatform, ) -> Result { - let fields = ManifestJsonParser::new(&self.text).parse_manifest()?; + let fields = ManifestJsonParser::new(self).parse_manifest()?; let host_name = NativeMessagingHostName::parse(&fields.name) .map_err(NativeMessagingManifestParseError::HostName)?; let allowed_origins: Vec<&str> = @@ -157,13 +157,17 @@ fn decoded_json_string(bytes: Vec) -> Result ManifestJsonParser<'a> { - const fn new(input: &'a str) -> Self { - Self { input, position: 0 } + fn new(document: &'a NativeMessagingManifestDocument) -> Self { + Self { + input: &document.text, + position: 0, + } } fn parse_manifest(mut self) -> Result { self.skip_whitespace(); - self.expect_byte(b'{')?; + // The document constructor already proved that the first non-whitespace byte is `{`. + self.position += 1; self.skip_whitespace(); let mut fields = PartialManifestFields::default(); if self.peek_byte() == Some(b'}') { @@ -328,9 +332,8 @@ impl<'a> ManifestJsonParser<'a> { &mut self, output: &mut Vec, ) -> Result<(), NativeMessagingManifestParseError> { - let Some(escape) = self.take_byte() else { - return Err(NativeMessagingManifestParseError::InvalidJson); - }; + // NUL is not a legal JSON escape, so unexpected EOF shares the normal fail-closed path. + let escape = self.take_byte().unwrap_or(b'\0'); match escape { b'"' => output.push(b'"'), b'\\' => output.push(b'\\'), @@ -513,23 +516,27 @@ mod tests { const INVALID_JSON: NativeMessagingManifestParseError = NativeMessagingManifestParseError::InvalidJson; + fn parse_manifest_for_test( + raw: &str, + ) -> Result { + let document = NativeMessagingManifestDocument::parse(raw.as_bytes()) + .expect("manifest parser fixture must satisfy the bounded outer-object invariant"); + ManifestJsonParser::new(&document).parse_manifest() + } + #[test] fn parser_propagates_structural_and_typed_field_failures() { for raw in [ - "[]", "{?}", r#"{"name" "value"}"#, r#"{"path":"\q"}"#, r#"{"type":"\q"}"#, ] { - assert_eq!( - ManifestJsonParser::new(raw).parse_manifest(), - Err(INVALID_JSON) - ); + assert_eq!(parse_manifest_for_test(raw), Err(INVALID_JSON)); } assert_eq!( - ManifestJsonParser::new(r#"{"name":1}"#).parse_manifest(), + parse_manifest_for_test(r#"{"name":1}"#), Err(NativeMessagingManifestParseError::InvalidFieldType) ); } @@ -537,25 +544,37 @@ mod tests { #[test] fn parser_propagates_array_escape_unicode_and_byte_boundary_failures() { assert_eq!( - ManifestJsonParser::new(r#"{"allowed_origins":["\q"]}"#).parse_manifest(), + parse_manifest_for_test(r#"{"allowed_origins":["\q"]}"#), Err(INVALID_JSON) ); assert_eq!( - ManifestJsonParser::new(r#"{"allowed_origins":["origin",]}"#).parse_manifest(), + parse_manifest_for_test(r#"{"allowed_origins":["origin",]}"#), Err(INVALID_JSON) ); - let mut trailing_array = ManifestJsonParser::new(r#"["origin",]"#); + let mut trailing_array = ManifestJsonParser { + input: r#"["origin",]"#, + position: 0, + }; assert_eq!(trailing_array.parse_string_array(), Err(INVALID_JSON)); - let mut empty_escape = ManifestJsonParser::new(""); + let mut empty_escape = ManifestJsonParser { + input: "", + position: 0, + }; let mut output = Vec::new(); assert_eq!(empty_escape.parse_escape(&mut output), Err(INVALID_JSON)); - let mut short_quad = ManifestJsonParser::new("12"); + let mut short_quad = ManifestJsonParser { + input: "12", + position: 0, + }; assert_eq!(short_quad.parse_hex_quad(), Err(INVALID_JSON)); - let mut short_second_quad = ManifestJsonParser::new("D83D\\u12"); + let mut short_second_quad = ManifestJsonParser { + input: "D83D\\u12", + position: 0, + }; assert_eq!(short_second_quad.parse_unicode_escape(), Err(INVALID_JSON)); assert_eq!(decoded_json_string(vec![0xff]), Err(INVALID_JSON)); From df71b52c7e733439a99c0d4cc4637427637b2e5d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:15:24 -0700 Subject: [PATCH 45/64] fix(core): avoid panic-only manifest test helper --- .../src/native_messaging_manifest_document.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index f64c9b278..4b7495340 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -519,8 +519,9 @@ mod tests { fn parse_manifest_for_test( raw: &str, ) -> Result { - let document = NativeMessagingManifestDocument::parse(raw.as_bytes()) - .expect("manifest parser fixture must satisfy the bounded outer-object invariant"); + let Ok(document) = NativeMessagingManifestDocument::parse(raw.as_bytes()) else { + return Err(INVALID_JSON); + }; ManifestJsonParser::new(&document).parse_manifest() } From ba269ffa144e73546b181b402e43ab50f559aeda Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 00:21:52 -0700 Subject: [PATCH 46/64] test(core): cover manifest helper pre-parser failure --- .../originweave-core/src/native_messaging_manifest_document.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index 4b7495340..1083589a7 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -528,6 +528,7 @@ mod tests { #[test] fn parser_propagates_structural_and_typed_field_failures() { for raw in [ + "", "{?}", r#"{"name" "value"}"#, r#"{"path":"\q"}"#, From 6631e6e7184f8c09c831d9b106d100ac62a6b7b4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 21:31:45 -0700 Subject: [PATCH 47/64] test(extension): bound manifest origins before excess decode --- ...native_messaging_manifest_origin_budget.rs | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_manifest_origin_budget.rs diff --git a/crates/originweave-core/tests/native_messaging_manifest_origin_budget.rs b/crates/originweave-core/tests/native_messaging_manifest_origin_budget.rs new file mode 100644 index 000000000..adb57fe3d --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_manifest_origin_budget.rs @@ -0,0 +1,25 @@ +#![allow(clippy::expect_used)] + +use originweave_core::{ + NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, +}; + +const EXTENSION_ORIGIN: &str = "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"; + +#[test] +fn complete_parser_enforces_origin_budget_before_decoding_excess_element() { + let allowed_origins = vec![format!("\"{EXTENSION_ORIGIN}\""); 256].join(","); + let raw = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[{allowed_origins},1]}}"# + ); + + let error = NativeMessagingManifestDocument::parse(raw.as_bytes()) + .expect("bounded over-budget fixture must pass the document pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect_err("the 257th origin entry must fail at the origin-count budget before decoding"); + + assert!(matches!( + error, + NativeMessagingManifestParseError::Manifest(_) + )); +} From d9b094a7d220488233adb226c8ef339154b88728 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 21:35:22 -0700 Subject: [PATCH 48/64] fix(extension): enforce manifest origin budget during parsing --- .../src/native_messaging_manifest_document.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index 1083589a7..ffa6bfd6b 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -10,7 +10,7 @@ use std::fmt; use crate::{ NativeMessagingHostManifest, NativeMessagingHostManifestError, NativeMessagingHostName, - NativeMessagingHostNameError, NativeMessagingHostPlatform, + NativeMessagingHostNameError, NativeMessagingHostPlatform, MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, }; /// Maximum UTF-8 byte length accepted for one native-messaging host manifest document. @@ -269,6 +269,11 @@ impl<'a> ManifestJsonParser<'a> { return Ok(values); } loop { + if values.len() == MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS { + return Err(NativeMessagingManifestParseError::Manifest( + NativeMessagingHostManifestError::TooManyAllowedOrigins, + )); + } if self.peek_byte() != Some(b'"') { return Err(NativeMessagingManifestParseError::InvalidFieldType); } From c2d466861f183dee5db81383e3d641869084989c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 21:39:16 -0700 Subject: [PATCH 49/64] chore(extension): apply canonical rustfmt ordering --- .../src/native_messaging_manifest_document.rs | 39 +++---------------- 1 file changed, 6 insertions(+), 33 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index ffa6bfd6b..31044809e 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -9,8 +9,9 @@ use std::fmt; use crate::{ - NativeMessagingHostManifest, NativeMessagingHostManifestError, NativeMessagingHostName, - NativeMessagingHostNameError, NativeMessagingHostPlatform, MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, + MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, NativeMessagingHostManifest, + NativeMessagingHostManifestError, NativeMessagingHostName, NativeMessagingHostNameError, + NativeMessagingHostPlatform, }; /// Maximum UTF-8 byte length accepted for one native-messaging host manifest document. @@ -166,7 +167,6 @@ impl<'a> ManifestJsonParser<'a> { fn parse_manifest(mut self) -> Result { self.skip_whitespace(); - // The document constructor already proved that the first non-whitespace byte is `{`. self.position += 1; self.skip_whitespace(); let mut fields = PartialManifestFields::default(); @@ -337,7 +337,6 @@ impl<'a> ManifestJsonParser<'a> { &mut self, output: &mut Vec, ) -> Result<(), NativeMessagingManifestParseError> { - // NUL is not a legal JSON escape, so unexpected EOF shares the normal fail-closed path. let escape = self.take_byte().unwrap_or(b'\0'); match escape { b'"' => output.push(b'"'), @@ -419,16 +418,11 @@ impl<'a> ManifestJsonParser<'a> { } } -/// Failure to admit a native-messaging host manifest document at the pre-parser boundary. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NativeMessagingManifestDocumentError { - /// The manifest document contained zero bytes. EmptyDocument, - /// The manifest document exceeded the OriginWeave pre-parser safety budget. DocumentTooLarge, - /// The manifest document was not valid UTF-8. InvalidUtf8, - /// The document did not have one outer object boundary after JSON whitespace was removed. InvalidObjectBoundary, } @@ -453,22 +447,14 @@ impl fmt::Display for NativeMessagingManifestDocumentError { impl std::error::Error for NativeMessagingManifestDocumentError {} -/// Failure to parse or validate a complete bounded native-messaging host manifest document. #[derive(Debug, Clone, PartialEq, Eq)] pub enum NativeMessagingManifestParseError { - /// The document was not one complete valid JSON object in the reviewed schema. InvalidJson, - /// A decoded manifest member appeared more than once. DuplicateField, - /// The manifest contained a member outside the reviewed Chrome native-host schema. UnknownField, - /// One or more Chrome-required manifest members were absent. MissingRequiredField, - /// A reviewed member used a JSON type different from the Chrome manifest contract. InvalidFieldType, - /// The decoded host-name string violated the existing exact host-identity contract. HostName(NativeMessagingHostNameError), - /// The decoded authority-bearing fields failed the existing host-manifest validator. Manifest(NativeMessagingHostManifestError), } @@ -532,16 +518,9 @@ mod tests { #[test] fn parser_propagates_structural_and_typed_field_failures() { - for raw in [ - "", - "{?}", - r#"{"name" "value"}"#, - r#"{"path":"\q"}"#, - r#"{"type":"\q"}"#, - ] { + for raw in ["", "{?}", r#"{"name" "value"}"#, r#"{"path":"\q"}"#, r#"{"type":"\q"}"#] { assert_eq!(parse_manifest_for_test(raw), Err(INVALID_JSON)); } - assert_eq!( parse_manifest_for_test(r#"{"name":1}"#), Err(NativeMessagingManifestParseError::InvalidFieldType) @@ -565,17 +544,11 @@ mod tests { }; assert_eq!(trailing_array.parse_string_array(), Err(INVALID_JSON)); - let mut empty_escape = ManifestJsonParser { - input: "", - position: 0, - }; + let mut empty_escape = ManifestJsonParser { input: "", position: 0 }; let mut output = Vec::new(); assert_eq!(empty_escape.parse_escape(&mut output), Err(INVALID_JSON)); - let mut short_quad = ManifestJsonParser { - input: "12", - position: 0, - }; + let mut short_quad = ManifestJsonParser { input: "12", position: 0 }; assert_eq!(short_quad.parse_hex_quad(), Err(INVALID_JSON)); let mut short_second_quad = ManifestJsonParser { From afaa4b0688cffa063efce5e52b2e431ae123557d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 19 Aug 2026 21:40:08 -0700 Subject: [PATCH 50/64] fix(extension): preserve parser contracts while formatting --- .../src/native_messaging_manifest_document.rs | 34 +++++++++++++++++-- 1 file changed, 31 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index 31044809e..dc701fc5b 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -167,6 +167,7 @@ impl<'a> ManifestJsonParser<'a> { fn parse_manifest(mut self) -> Result { self.skip_whitespace(); + // The document constructor already proved that the first non-whitespace byte is `{`. self.position += 1; self.skip_whitespace(); let mut fields = PartialManifestFields::default(); @@ -337,6 +338,7 @@ impl<'a> ManifestJsonParser<'a> { &mut self, output: &mut Vec, ) -> Result<(), NativeMessagingManifestParseError> { + // NUL is not a legal JSON escape, so unexpected EOF shares the normal fail-closed path. let escape = self.take_byte().unwrap_or(b'\0'); match escape { b'"' => output.push(b'"'), @@ -418,11 +420,16 @@ impl<'a> ManifestJsonParser<'a> { } } +/// Failure to admit a native-messaging host manifest document at the pre-parser boundary. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum NativeMessagingManifestDocumentError { + /// The manifest document contained zero bytes. EmptyDocument, + /// The manifest document exceeded the OriginWeave pre-parser safety budget. DocumentTooLarge, + /// The manifest document was not valid UTF-8. InvalidUtf8, + /// The document did not have one outer object boundary after JSON whitespace was removed. InvalidObjectBoundary, } @@ -447,14 +454,22 @@ impl fmt::Display for NativeMessagingManifestDocumentError { impl std::error::Error for NativeMessagingManifestDocumentError {} +/// Failure to parse or validate a complete bounded native-messaging host manifest document. #[derive(Debug, Clone, PartialEq, Eq)] pub enum NativeMessagingManifestParseError { + /// The document was not one complete valid JSON object in the reviewed schema. InvalidJson, + /// A decoded manifest member appeared more than once. DuplicateField, + /// The manifest contained a member outside the reviewed Chrome native-host schema. UnknownField, + /// One or more Chrome-required manifest members were absent. MissingRequiredField, + /// A reviewed member used a JSON type different from the Chrome manifest contract. InvalidFieldType, + /// The decoded host-name string violated the existing exact host-identity contract. HostName(NativeMessagingHostNameError), + /// The decoded authority-bearing fields failed the existing host-manifest validator. Manifest(NativeMessagingHostManifestError), } @@ -518,9 +533,16 @@ mod tests { #[test] fn parser_propagates_structural_and_typed_field_failures() { - for raw in ["", "{?}", r#"{"name" "value"}"#, r#"{"path":"\q"}"#, r#"{"type":"\q"}"#] { + for raw in [ + "", + "{?}", + r#"{"name" "value"}"#, + r#"{"path":"\q"}"#, + r#"{"type":"\q"}"#, + ] { assert_eq!(parse_manifest_for_test(raw), Err(INVALID_JSON)); } + assert_eq!( parse_manifest_for_test(r#"{"name":1}"#), Err(NativeMessagingManifestParseError::InvalidFieldType) @@ -544,11 +566,17 @@ mod tests { }; assert_eq!(trailing_array.parse_string_array(), Err(INVALID_JSON)); - let mut empty_escape = ManifestJsonParser { input: "", position: 0 }; + let mut empty_escape = ManifestJsonParser { + input: "", + position: 0, + }; let mut output = Vec::new(); assert_eq!(empty_escape.parse_escape(&mut output), Err(INVALID_JSON)); - let mut short_quad = ManifestJsonParser { input: "12", position: 0 }; + let mut short_quad = ManifestJsonParser { + input: "12", + position: 0, + }; assert_eq!(short_quad.parse_hex_quad(), Err(INVALID_JSON)); let mut short_second_quad = ManifestJsonParser { From 33b8329e1c5b2125ccf44d756d8e510e8884484b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 05:32:29 -0700 Subject: [PATCH 51/64] test(native-messaging): reject empty host descriptions --- .../native_messaging_manifest_description.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 crates/originweave-core/tests/native_messaging_manifest_description.rs diff --git a/crates/originweave-core/tests/native_messaging_manifest_description.rs b/crates/originweave-core/tests/native_messaging_manifest_description.rs new file mode 100644 index 000000000..0c3ee2bf8 --- /dev/null +++ b/crates/originweave-core/tests/native_messaging_manifest_description.rs @@ -0,0 +1,23 @@ +use originweave_core::{ + NativeMessagingHostPlatform, NativeMessagingManifestDocument, + NativeMessagingManifestParseError, +}; + +#[test] +fn empty_native_messaging_host_description_is_not_chrome_valid() { + let document = NativeMessagingManifestDocument::parse( + br#"{ + "name":"com.contextualwisdom.originweave", + "description":"", + "path":"/opt/originweave/native-host", + "type":"stdio", + "allowed_origins":["chrome-extension://abcdefghijklmnopabcdefghijklmnop/"] + }"#, + ) + .expect("the bounded document is syntactically object-shaped"); + + assert_eq!( + document.parse_host_manifest(NativeMessagingHostPlatform::Linux), + Err(NativeMessagingManifestParseError::InvalidFieldValue) + ); +} From 1d384f31019671986161801a0a55c4724718d39e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 05:33:39 -0700 Subject: [PATCH 52/64] style(native-messaging): format description regression --- .../tests/native_messaging_manifest_description.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_description.rs b/crates/originweave-core/tests/native_messaging_manifest_description.rs index 0c3ee2bf8..b3964aaad 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_description.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_description.rs @@ -1,6 +1,5 @@ use originweave_core::{ - NativeMessagingHostPlatform, NativeMessagingManifestDocument, - NativeMessagingManifestParseError, + NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, }; #[test] From a72967e26b4bf5301da7d1afec341b1b9c6d7aa3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 05:37:49 -0700 Subject: [PATCH 53/64] test(native-messaging): cover invalid description error --- .../tests/native_messaging_manifest_description.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_description.rs b/crates/originweave-core/tests/native_messaging_manifest_description.rs index b3964aaad..1e7e41131 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_description.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_description.rs @@ -1,3 +1,5 @@ +use std::error::Error; + use originweave_core::{ NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, }; @@ -15,8 +17,16 @@ fn empty_native_messaging_host_description_is_not_chrome_valid() { ) .expect("the bounded document is syntactically object-shaped"); + let error = document + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect_err("Chrome rejects an empty required native-host description"); + assert_eq!( + error, + NativeMessagingManifestParseError::InvalidFieldValue + ); assert_eq!( - document.parse_host_manifest(NativeMessagingHostPlatform::Linux), - Err(NativeMessagingManifestParseError::InvalidFieldValue) + error.to_string(), + "native messaging host manifest field has an invalid value" ); + assert!(error.source().is_none()); } From e9ed78b7d0a5dbf38d285d089a2353b9d1c34c23 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 05:39:14 -0700 Subject: [PATCH 54/64] fix(native-messaging): reject empty host descriptions --- .../src/native_messaging_manifest_document.rs | 23 +++++++++++++------ 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-core/src/native_messaging_manifest_document.rs index dc701fc5b..97dc0bb0e 100644 --- a/crates/originweave-core/src/native_messaging_manifest_document.rs +++ b/crates/originweave-core/src/native_messaging_manifest_document.rs @@ -67,11 +67,11 @@ impl NativeMessagingManifestDocument { /// commas, and trailing JSON data all fail closed. JSON strings are decoded before the /// existing host-name, path, interface, and extension-origin validators run. /// - /// `description` is required and type-checked because Chrome's manifest schema requires it, - /// but it is intentionally not retained as authority. The optional native-initiated field - /// defaults to `false` when absent. A successful result still does not prove installation, - /// filesystem ownership, executable identity, process provenance, feature/policy enablement, - /// message provenance, or Agent authority. + /// `description` is required, type-checked, and non-empty because Chrome's manifest schema + /// requires a non-empty description, but it is intentionally not retained as authority. The + /// optional native-initiated field defaults to `false` when absent. A successful result still + /// does not prove installation, filesystem ownership, executable identity, process provenance, + /// feature/policy enablement, message provenance, or Agent authority. pub fn parse_host_manifest( &self, platform: NativeMessagingHostPlatform, @@ -122,7 +122,7 @@ impl PartialManifestFields { fn finish(self) -> Result { let ( Some(name), - Some(_description), + Some(description), Some(executable_path), Some(interface_type), Some(allowed_origins), @@ -136,6 +136,9 @@ impl PartialManifestFields { else { return Err(NativeMessagingManifestParseError::MissingRequiredField); }; + if description.is_empty() { + return Err(NativeMessagingManifestParseError::InvalidFieldValue); + } Ok(ManifestFields { name, executable_path, @@ -467,6 +470,8 @@ pub enum NativeMessagingManifestParseError { MissingRequiredField, /// A reviewed member used a JSON type different from the Chrome manifest contract. InvalidFieldType, + /// A reviewed member used a JSON value rejected by the Chrome manifest contract. + InvalidFieldValue, /// The decoded host-name string violated the existing exact host-identity contract. HostName(NativeMessagingHostNameError), /// The decoded authority-bearing fields failed the existing host-manifest validator. @@ -491,6 +496,9 @@ impl fmt::Display for NativeMessagingManifestParseError { Self::InvalidFieldType => { formatter.write_str("native messaging host manifest field has an invalid JSON type") } + Self::InvalidFieldValue => { + formatter.write_str("native messaging host manifest field has an invalid value") + } Self::HostName(error) => { write!(formatter, "invalid native messaging host name: {error}") } @@ -510,7 +518,8 @@ impl std::error::Error for NativeMessagingManifestParseError { | Self::DuplicateField | Self::UnknownField | Self::MissingRequiredField - | Self::InvalidFieldType => None, + | Self::InvalidFieldType + | Self::InvalidFieldValue => None, } } } From 7cddb519e969c33bd64d1224e01f15a08f220816 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 05:40:56 -0700 Subject: [PATCH 55/64] style(native-messaging): format description error coverage --- .../tests/native_messaging_manifest_description.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_description.rs b/crates/originweave-core/tests/native_messaging_manifest_description.rs index 1e7e41131..2c678ebe2 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_description.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_description.rs @@ -20,10 +20,7 @@ fn empty_native_messaging_host_description_is_not_chrome_valid() { let error = document .parse_host_manifest(NativeMessagingHostPlatform::Linux) .expect_err("Chrome rejects an empty required native-host description"); - assert_eq!( - error, - NativeMessagingManifestParseError::InvalidFieldValue - ); + assert_eq!(error, NativeMessagingManifestParseError::InvalidFieldValue); assert_eq!( error.to_string(), "native messaging host manifest field has an invalid value" From 9a84b16ca32181502109fb54a9eec44691954f2b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 20 Aug 2026 05:44:20 -0700 Subject: [PATCH 56/64] test(native-messaging): keep description regression lint-clean --- .../tests/native_messaging_manifest_description.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/originweave-core/tests/native_messaging_manifest_description.rs b/crates/originweave-core/tests/native_messaging_manifest_description.rs index 2c678ebe2..fc6a8cc1e 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_description.rs +++ b/crates/originweave-core/tests/native_messaging_manifest_description.rs @@ -6,7 +6,7 @@ use originweave_core::{ #[test] fn empty_native_messaging_host_description_is_not_chrome_valid() { - let document = NativeMessagingManifestDocument::parse( + let result = NativeMessagingManifestDocument::parse( br#"{ "name":"com.contextualwisdom.originweave", "description":"", @@ -15,12 +15,14 @@ fn empty_native_messaging_host_description_is_not_chrome_valid() { "allowed_origins":["chrome-extension://abcdefghijklmnopabcdefghijklmnop/"] }"#, ) - .expect("the bounded document is syntactically object-shaped"); + .map(|document| document.parse_host_manifest(NativeMessagingHostPlatform::Linux)); - let error = document - .parse_host_manifest(NativeMessagingHostPlatform::Linux) - .expect_err("Chrome rejects an empty required native-host description"); - assert_eq!(error, NativeMessagingManifestParseError::InvalidFieldValue); + assert!(matches!( + result, + Ok(Err(NativeMessagingManifestParseError::InvalidFieldValue)) + )); + + let error = NativeMessagingManifestParseError::InvalidFieldValue; assert_eq!( error.to_string(), "native messaging host manifest field has an invalid value" From a0fb625d7c429be27ee2525da9644a756116dac5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 03:31:44 -0700 Subject: [PATCH 57/64] fix(stack): preserve current native messaging framing truth --- crates/originweave-core/src/native_messaging.rs | 4 ++-- docs/doctoring/mv3-compatibility.md | 6 +++++- 2 files changed, 7 insertions(+), 3 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], 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 1cd62ee1042f9e63852f55da0ec01166cf5e9ba5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 03:33:03 -0700 Subject: [PATCH 58/64] docs: preserve parent changelog and record native manifest authority --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 12946290b..2e6f3bc52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,8 @@ All notable changes to OriginWeave are documented in this file. The format follo - 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. +- 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. +- Added fail-closed native-messaging host-manifest parsing with a 64 KiB ingress budget, exact `stdio` and extension-origin admission, bounded executable-path and allow-list inputs, strict JSON structure, and no implication of installation, process, message, secret, or 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 b86d094eebe9f45476fe1fd0942bb74340b6eef6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 24 Aug 2026 03:48:33 -0700 Subject: [PATCH 59/64] docs(stack): preserve current framing changelog truth --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e6f3bc52..633e77b79 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,12 +12,12 @@ 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. - Added fail-closed native-messaging host-manifest parsing with a 64 KiB ingress budget, exact `stdio` and extension-origin admission, bounded executable-path and allow-list inputs, strict JSON structure, and no implication of installation, process, message, secret, or 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. - Typed browser actions, capabilities, risk classes, execution modes, robots decisions, secret-delivery contracts, immutable canonical action-intent digests, and intent-bound approval scopes. -- Protected main now contains deterministic MCP `2026-07-28` stateless `tools/call` routing with bounded method/tool names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. The complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned. +- Protected main contains deterministic MCP `2026-07-28` stateless tool-routing foundations with bounded names, a single reviewed tool-to-action registry shared by routing and discovery metadata, and fail-closed policy binding that grants no ambient authority. The complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned. - Deterministic fail-closed policy evaluation for untrusted instructions, origin grants, crawler restrictions, execution-mode and purpose consistency, approvals, and brokered secrets. - Fail-closed resolved-destination policy with IPv4/IPv6 special-purpose and reviewed cloud-platform endpoint classification, IPv4-mapped canonicalization, explicit class grants, non-empty origin-bound DNS snapshots capped at 256 resolver addresses, concrete connection pinning, DNS-set expansion detection, and per-hop redirect reauthorization. - Direct-only `originweave-network` TCP boundary with explicit canonical `SocketAddr` authority, zero IPv6 flow and scope metadata unless separately modeled, a non-cloneable single-use plan, a 30-second per-attempt timeout ceiling, at most four attempts, exact `peer_addr` verification before stream exposure, and no hostname re-resolution or ambient proxy inheritance. From 538ca90503205ca12a718a1182f35194bbf8b980 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:19:01 -0700 Subject: [PATCH 60/64] test(native-messaging): bound descendant stream read requests --- .../tests/native_messaging_framing.rs | 45 ++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) 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 = From 545aa47f204753e12304a527e724cbee55386021 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 29 Aug 2026 05:19:44 -0700 Subject: [PATCH 61/64] fix(native-messaging): preserve bounded descendant stream reads --- crates/originweave-core/src/native_messaging.rs | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) 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) } From bdfb5799a95555765cfbc5d3f5028c5fe319b493 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 05:45:20 +0900 Subject: [PATCH 62/64] test(ddd): require native manifest extension ownership --- ...messaging_manifest_context_architecture.py | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/test_native_messaging_manifest_context_architecture.py diff --git a/tests/test_native_messaging_manifest_context_architecture.py b/tests/test_native_messaging_manifest_context_architecture.py new file mode 100644 index 000000000..3e256d918 --- /dev/null +++ b/tests/test_native_messaging_manifest_context_architecture.py @@ -0,0 +1,32 @@ +"""Architectural fitness for native-messaging manifest ownership.""" + +from __future__ import annotations + +import unittest +from pathlib import Path + + +ROOT = Path(__file__).resolve().parents[1] + + +class NativeMessagingManifestContextArchitectureTest(unittest.TestCase): + """Keep Chrome native-host manifest behavior inside the Extension Policy context.""" + + def test_manifest_behavior_belongs_to_extension_context(self) -> None: + """Host-manifest parsing must not leak back into stable core contracts.""" + workspace = (ROOT / "Cargo.toml").read_text(encoding="utf-8") + self.assertIn('"crates/originweave-extension"', workspace) + + extension_source = ROOT / "crates" / "originweave-extension" / "src" + self.assertTrue((extension_source / "native_messaging_manifest.rs").is_file()) + self.assertTrue((extension_source / "native_messaging_manifest_document.rs").is_file()) + + core_source = ROOT / "crates" / "originweave-core" / "src" + self.assertFalse((core_source / "native_messaging_manifest.rs").exists()) + self.assertFalse((core_source / "native_messaging_manifest_document.rs").exists()) + core_entry = (core_source / "root.rs").read_text(encoding="utf-8") + self.assertNotIn("native_messaging_manifest", core_entry) + + +if __name__ == "__main__": + unittest.main() From 667d26244507ed9b18510a521bf9aba4b3a19b98 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 08:26:08 +0900 Subject: [PATCH 63/64] refactor(extension): own native host manifest policy --- Cargo.lock | 9 +- Cargo.toml | 1 + crates/originweave-core/src/root.rs | 13 -- ...ative_messaging_manifest_document_edges.rs | 197 ------------------ crates/originweave-extension/Cargo.toml | 20 ++ .../src/native_messaging_manifest.rs | 0 .../src/native_messaging_manifest_document.rs | 0 crates/originweave-extension/src/root.rs | 25 +++ .../native_messaging_manifest_authority.rs | 2 +- .../native_messaging_manifest_description.rs | 2 +- .../native_messaging_manifest_document.rs | 2 +- ...ative_messaging_manifest_document_edges.rs | 125 +++++++++++ ...native_messaging_manifest_origin_budget.rs | 2 +- ...tive_messaging_manifest_syntax_boundary.rs | 2 +- 14 files changed, 184 insertions(+), 216 deletions(-) delete mode 100644 crates/originweave-core/tests/native_messaging_manifest_document_edges.rs create mode 100644 crates/originweave-extension/Cargo.toml rename crates/{originweave-core => originweave-extension}/src/native_messaging_manifest.rs (100%) rename crates/{originweave-core => originweave-extension}/src/native_messaging_manifest_document.rs (100%) create mode 100644 crates/originweave-extension/src/root.rs rename crates/{originweave-core => originweave-extension}/tests/native_messaging_manifest_authority.rs (99%) rename crates/{originweave-core => originweave-extension}/tests/native_messaging_manifest_description.rs (97%) rename crates/{originweave-core => originweave-extension}/tests/native_messaging_manifest_document.rs (99%) create mode 100644 crates/originweave-extension/tests/native_messaging_manifest_document_edges.rs rename crates/{originweave-core => originweave-extension}/tests/native_messaging_manifest_origin_budget.rs (97%) rename crates/{originweave-core => originweave-extension}/tests/native_messaging_manifest_syntax_boundary.rs (98%) diff --git a/Cargo.lock b/Cargo.lock index 848cb7320..75671577a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -288,6 +288,13 @@ dependencies = [ "originweave-core", ] +[[package]] +name = "originweave-extension" +version = "0.1.0" +dependencies = [ + "originweave-core", +] + [[package]] name = "originweave-network" version = "0.1.0" @@ -574,7 +581,7 @@ dependencies = [ name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "1f3ccbac311fea05f86c61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "typenum" diff --git a/Cargo.toml b/Cargo.toml index 0d5ab469c..7c7a78e49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "crates/originweave-core", + "crates/originweave-extension", "crates/originweave-bap", "crates/originweave-policy", "crates/originweave-resource", diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs index 67ed5c694..60d659105 100644 --- a/crates/originweave-core/src/root.rs +++ b/crates/originweave-core/src/root.rs @@ -16,18 +16,5 @@ pub mod mcp; mod native_messaging; pub use native_messaging::*; -mod native_messaging_manifest; -pub use native_messaging_manifest::{ - MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, MAX_NATIVE_MESSAGING_EXECUTABLE_PATH_BYTES, - NativeMessagingHostManifest, NativeMessagingHostManifestAccessDecision, - NativeMessagingHostManifestError, NativeMessagingHostPlatform, -}; - -mod native_messaging_manifest_document; -pub use native_messaging_manifest_document::{ - MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES, NativeMessagingManifestDocument, - NativeMessagingManifestDocumentError, NativeMessagingManifestParseError, -}; - /// Deterministic fail-closed release benchmark acceptance aggregation. pub mod release_acceptance; diff --git a/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs b/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs deleted file mode 100644 index ba05b983c..000000000 --- a/crates/originweave-core/tests/native_messaging_manifest_document_edges.rs +++ /dev/null @@ -1,197 +0,0 @@ -#![allow(clippy::expect_used)] - -use std::error::Error; - -use originweave_core::{ - NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, -}; - -const EXTENSION_ORIGIN: &str = "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"; -const SECOND_EXTENSION_ORIGIN: &str = "chrome-extension://ponmlkjihgfedcbaponmlkjihgfedcba/"; - -fn parse_error(raw: &str) -> NativeMessagingManifestParseError { - NativeMessagingManifestDocument::parse(raw.as_bytes()) - .expect("edge fixture must pass only the bounded outer-object pre-parser") - .parse_host_manifest(NativeMessagingHostPlatform::Linux) - .expect_err("edge fixture must fail complete manifest parsing or authority validation") -} - -#[test] -fn complete_parser_rejects_every_duplicate_reviewed_field() { - let cases = [ - format!( - r#"{{"name":"com.contextualwisdom.originweave","name":"com.contextualwisdom.other","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# - ), - format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","description":"other","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# - ), - format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","path":"/tmp/other","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# - ), - format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# - ), - format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"allowed_origins":["{EXTENSION_ORIGIN}"]}}"# - ), - format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":true,"supports_native_initiated_connections":false}}"# - ), - ]; - - for raw in cases { - assert_eq!( - parse_error(&raw), - NativeMessagingManifestParseError::DuplicateField - ); - } -} - -#[test] -fn complete_parser_covers_empty_and_malformed_array_and_boolean_shapes() { - let empty_object = NativeMessagingManifestDocument::parse(b"{}") - .expect("empty object passes only the bounded outer-object pre-parser"); - assert_eq!( - empty_object.parse_host_manifest(NativeMessagingHostPlatform::Linux), - Err(NativeMessagingManifestParseError::MissingRequiredField) - ); - - let empty_origins = r#"{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[]}"#.to_owned(); - assert!(matches!( - parse_error(&empty_origins), - NativeMessagingManifestParseError::Manifest(_) - )); - - let multiple_origins = format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}","{SECOND_EXTENSION_ORIGIN}"]}}"# - ); - let multiple_manifest = NativeMessagingManifestDocument::parse(multiple_origins.as_bytes()) - .expect("valid multi-origin fixture passes pre-parser") - .parse_host_manifest(NativeMessagingHostPlatform::Linux) - .expect("valid multi-origin fixture passes complete parsing"); - assert_eq!(multiple_manifest.allowed_extension_count(), 2); - - for raw in [ - r#"{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[true]}"#.to_owned(), - format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":"{EXTENSION_ORIGIN}"}}"# - ), - ] { - assert_eq!( - parse_error(&raw), - NativeMessagingManifestParseError::InvalidFieldType - ); - } - - for raw in [ - format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}",]}}"# - ), - format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}" "{EXTENSION_ORIGIN}"]}}"# - ), - "{} {}".to_owned(), - ] { - assert_eq!( - parse_error(&raw), - NativeMessagingManifestParseError::InvalidJson - ); - } - - let invalid_boolean = format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":1}}"# - ); - assert_eq!( - parse_error(&invalid_boolean), - NativeMessagingManifestParseError::InvalidFieldType - ); - - let false_boolean = format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":false}}"# - ); - let manifest = NativeMessagingManifestDocument::parse(false_boolean.as_bytes()) - .expect("valid false-boolean fixture passes pre-parser") - .parse_host_manifest(NativeMessagingHostPlatform::Linux) - .expect("valid false-boolean fixture passes complete parsing"); - assert!(!manifest.supports_native_initiated_connections()); -} - -#[test] -fn complete_parser_covers_json_escape_and_unicode_failure_edges() { - let escaped_description = format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"a\b\f\n\r\t-\u0041-\u00E9-\u263A-\uD83D\uDE00-\u00AF","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# - ); - let manifest = NativeMessagingManifestDocument::parse(escaped_description.as_bytes()) - .expect("valid escaped-string fixture passes pre-parser") - .parse_host_manifest(NativeMessagingHostPlatform::Linux) - .expect("valid escaped-string fixture passes complete parsing"); - assert_eq!(manifest.allowed_extension_count(), 1); - - for description in [ - r#"bad\q"#, - r#"bad\uD83D"#, - r#"bad\uD83D\x0000"#, - r#"bad\uD83D\u0041"#, - r#"bad\uDE00"#, - r#"bad\u12"#, - r#"bad\u00G0"#, - ] { - let raw = format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"{description}","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# - ); - assert_eq!( - parse_error(&raw), - NativeMessagingManifestParseError::InvalidJson - ); - } - - let raw_control = format!( - "{{\"name\":\"com.contextualwisdom.originweave\",\"description\":\"bad\u{0001}text\",\"path\":\"/opt/originweave/native-host\",\"type\":\"stdio\",\"allowed_origins\":[\"{EXTENSION_ORIGIN}\"]}}" - ); - assert_eq!( - parse_error(&raw_control), - NativeMessagingManifestParseError::InvalidJson - ); - - let unterminated = r#"{"name":"unterminated}"#; - assert_eq!( - parse_error(unterminated), - NativeMessagingManifestParseError::InvalidJson - ); -} - -#[test] -fn parse_errors_expose_deterministic_display_and_only_causal_sources() { - for error in [ - NativeMessagingManifestParseError::InvalidJson, - NativeMessagingManifestParseError::DuplicateField, - NativeMessagingManifestParseError::UnknownField, - NativeMessagingManifestParseError::MissingRequiredField, - NativeMessagingManifestParseError::InvalidFieldType, - ] { - assert!(!error.to_string().is_empty()); - assert!(error.source().is_none()); - } - - let invalid_host = format!( - r#"{{"name":"INVALID HOST","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# - ); - let host_error = parse_error(&invalid_host); - assert!(matches!( - host_error, - NativeMessagingManifestParseError::HostName(_) - )); - assert!(!host_error.to_string().is_empty()); - assert!(host_error.source().is_some()); - - let invalid_manifest = format!( - r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"pipe","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# - ); - let manifest_error = parse_error(&invalid_manifest); - assert!(matches!( - manifest_error, - NativeMessagingManifestParseError::Manifest(_) - )); - assert!(!manifest_error.to_string().is_empty()); - assert!(manifest_error.source().is_some()); -} diff --git a/crates/originweave-extension/Cargo.toml b/crates/originweave-extension/Cargo.toml new file mode 100644 index 000000000..fed097c52 --- /dev/null +++ b/crates/originweave-extension/Cargo.toml @@ -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 diff --git a/crates/originweave-core/src/native_messaging_manifest.rs b/crates/originweave-extension/src/native_messaging_manifest.rs similarity index 100% rename from crates/originweave-core/src/native_messaging_manifest.rs rename to crates/originweave-extension/src/native_messaging_manifest.rs diff --git a/crates/originweave-core/src/native_messaging_manifest_document.rs b/crates/originweave-extension/src/native_messaging_manifest_document.rs similarity index 100% rename from crates/originweave-core/src/native_messaging_manifest_document.rs rename to crates/originweave-extension/src/native_messaging_manifest_document.rs diff --git a/crates/originweave-extension/src/root.rs b/crates/originweave-extension/src/root.rs new file mode 100644 index 000000000..7f9cac6a0 --- /dev/null +++ b/crates/originweave-extension/src/root.rs @@ -0,0 +1,25 @@ +//! Extension-policy contracts for OriginWeave's Chromium compatibility boundary. +//! +//! This crate owns validated native-messaging host-manifest semantics. Stable identity and +//! request value objects remain in `originweave-core`; this context depends inward on those +//! contracts and does not grant process, browser-action, secret, or Agent authority. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +pub use originweave_core::{ + ExtensionId, NativeMessagingAccessRequest, NativeMessagingHostName, NativeMessagingHostNameError, +}; + +mod native_messaging_manifest; +pub use native_messaging_manifest::{ + MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, MAX_NATIVE_MESSAGING_EXECUTABLE_PATH_BYTES, + NativeMessagingHostManifest, NativeMessagingHostManifestAccessDecision, + NativeMessagingHostManifestError, NativeMessagingHostPlatform, +}; + +mod native_messaging_manifest_document; +pub use native_messaging_manifest_document::{ + MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES, NativeMessagingManifestDocument, + NativeMessagingManifestDocumentError, NativeMessagingManifestParseError, +}; diff --git a/crates/originweave-core/tests/native_messaging_manifest_authority.rs b/crates/originweave-extension/tests/native_messaging_manifest_authority.rs similarity index 99% rename from crates/originweave-core/tests/native_messaging_manifest_authority.rs rename to crates/originweave-extension/tests/native_messaging_manifest_authority.rs index 0e7dff59e..7df3f4025 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_authority.rs +++ b/crates/originweave-extension/tests/native_messaging_manifest_authority.rs @@ -2,7 +2,7 @@ use std::error::Error; -use originweave_core::{ +use originweave_extension::{ ExtensionId, MAX_NATIVE_MESSAGING_ALLOWED_ORIGINS, MAX_NATIVE_MESSAGING_EXECUTABLE_PATH_BYTES, NativeMessagingAccessRequest, NativeMessagingHostManifest, NativeMessagingHostManifestAccessDecision, NativeMessagingHostManifestError, diff --git a/crates/originweave-core/tests/native_messaging_manifest_description.rs b/crates/originweave-extension/tests/native_messaging_manifest_description.rs similarity index 97% rename from crates/originweave-core/tests/native_messaging_manifest_description.rs rename to crates/originweave-extension/tests/native_messaging_manifest_description.rs index fc6a8cc1e..ee525cdb3 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_description.rs +++ b/crates/originweave-extension/tests/native_messaging_manifest_description.rs @@ -1,6 +1,6 @@ use std::error::Error; -use originweave_core::{ +use originweave_extension::{ NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, }; diff --git a/crates/originweave-core/tests/native_messaging_manifest_document.rs b/crates/originweave-extension/tests/native_messaging_manifest_document.rs similarity index 99% rename from crates/originweave-core/tests/native_messaging_manifest_document.rs rename to crates/originweave-extension/tests/native_messaging_manifest_document.rs index 33b33f288..00a0cb755 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_document.rs +++ b/crates/originweave-extension/tests/native_messaging_manifest_document.rs @@ -2,7 +2,7 @@ use std::error::Error; -use originweave_core::{ +use originweave_extension::{ MAX_NATIVE_MESSAGING_MANIFEST_DOCUMENT_BYTES, NativeMessagingHostManifestError, NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestDocumentError, NativeMessagingManifestParseError, diff --git a/crates/originweave-extension/tests/native_messaging_manifest_document_edges.rs b/crates/originweave-extension/tests/native_messaging_manifest_document_edges.rs new file mode 100644 index 000000000..08b9af861 --- /dev/null +++ b/crates/originweave-extension/tests/native_messaging_manifest_document_edges.rs @@ -0,0 +1,125 @@ +#![allow(clippy::expect_used)] + +use std::error::Error; + +use originweave_extension::{ + NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, +}; + +const EXTENSION_ORIGIN: &str = "chrome-extension://abcdefghijklmnopabcdefghijklmnop/"; +const SECOND_EXTENSION_ORIGIN: &str = "chrome-extension://ponmlkjihgfedcbaponmlkjihgfedcba/"; + +fn parse_error(raw: &str) -> NativeMessagingManifestParseError { + NativeMessagingManifestDocument::parse(raw.as_bytes()) + .expect("edge fixture must pass only the bounded outer-object pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect_err("edge fixture must fail complete manifest parsing or authority validation") +} + +#[test] +fn complete_parser_rejects_every_duplicate_reviewed_field() { + let cases = [ + format!(r#"{{"name":"com.contextualwisdom.originweave","name":"com.contextualwisdom.other","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), + format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","description":"other","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), + format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","path":"/tmp/other","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), + format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), + format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), + format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":true,"supports_native_initiated_connections":false}}"#), + ]; + + for raw in cases { + assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::DuplicateField); + } +} + +#[test] +fn complete_parser_covers_empty_and_malformed_array_and_boolean_shapes() { + let empty_object = NativeMessagingManifestDocument::parse(b"{}") + .expect("empty object passes only the bounded outer-object pre-parser"); + assert_eq!( + empty_object.parse_host_manifest(NativeMessagingHostPlatform::Linux), + Err(NativeMessagingManifestParseError::MissingRequiredField) + ); + + let empty_origins = r#"{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[]}"#.to_owned(); + assert!(matches!(parse_error(&empty_origins), NativeMessagingManifestParseError::Manifest(_))); + + let multiple_origins = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}","{SECOND_EXTENSION_ORIGIN}"]}}"#); + let multiple_manifest = NativeMessagingManifestDocument::parse(multiple_origins.as_bytes()) + .expect("valid multi-origin fixture passes pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect("valid multi-origin fixture passes complete parsing"); + assert_eq!(multiple_manifest.allowed_extension_count(), 2); + + for raw in [ + r#"{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[true]}"#.to_owned(), + format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":"{EXTENSION_ORIGIN}"}}"#), + ] { + assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidFieldType); + } + + for raw in [ + format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}",]}}"#), + format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}" "{EXTENSION_ORIGIN}"]}}"#), + "{} {}".to_owned(), + ] { + assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidJson); + } + + let invalid_boolean = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":1}}"#); + assert_eq!(parse_error(&invalid_boolean), NativeMessagingManifestParseError::InvalidFieldType); + + let false_boolean = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":false}}"#); + let manifest = NativeMessagingManifestDocument::parse(false_boolean.as_bytes()) + .expect("valid false-boolean fixture passes pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect("valid false-boolean fixture passes complete parsing"); + assert!(!manifest.supports_native_initiated_connections()); +} + +#[test] +fn complete_parser_covers_json_escape_and_unicode_failure_edges() { + let escaped_description = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"a\b\f\n\r\t-\u0041-\u00E9-\u263A-\uD83D\uDE00-\u00AF","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#); + let manifest = NativeMessagingManifestDocument::parse(escaped_description.as_bytes()) + .expect("valid escaped-string fixture passes pre-parser") + .parse_host_manifest(NativeMessagingHostPlatform::Linux) + .expect("valid escaped-string fixture passes complete parsing"); + assert_eq!(manifest.allowed_extension_count(), 1); + + for description in [r#"bad\q"#, r#"bad\uD83D"#, r#"bad\uD83D\x0000"#, r#"bad\uD83D\u0041"#, r#"bad\uDE00"#, r#"bad\u12"#, r#"bad\u00G0"#] { + let raw = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"{description}","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#); + assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidJson); + } + + let raw_control = format!("{{\"name\":\"com.contextualwisdom.originweave\",\"description\":\"bad\u{0001}text\",\"path\":\"/opt/originweave/native-host\",\"type\":\"stdio\",\"allowed_origins\":[\"{EXTENSION_ORIGIN}\"]}}"); + assert_eq!(parse_error(&raw_control), NativeMessagingManifestParseError::InvalidJson); + + let unterminated = r#"{"name":"unterminated}"#; + assert_eq!(parse_error(unterminated), NativeMessagingManifestParseError::InvalidJson); +} + +#[test] +fn parse_errors_expose_deterministic_display_and_only_causal_sources() { + for error in [ + NativeMessagingManifestParseError::InvalidJson, + NativeMessagingManifestParseError::DuplicateField, + NativeMessagingManifestParseError::UnknownField, + NativeMessagingManifestParseError::MissingRequiredField, + NativeMessagingManifestParseError::InvalidFieldType, + ] { + assert!(!error.to_string().is_empty()); + assert!(error.source().is_none()); + } + + let invalid_host = format!(r#"{{"name":"INVALID HOST","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#); + let host_error = parse_error(&invalid_host); + assert!(matches!(host_error, NativeMessagingManifestParseError::HostName(_))); + assert!(!host_error.to_string().is_empty()); + assert!(host_error.source().is_some()); + + let invalid_manifest = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"pipe","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#); + let manifest_error = parse_error(&invalid_manifest); + assert!(matches!(manifest_error, NativeMessagingManifestParseError::Manifest(_))); + assert!(!manifest_error.to_string().is_empty()); + assert!(manifest_error.source().is_some()); +} diff --git a/crates/originweave-core/tests/native_messaging_manifest_origin_budget.rs b/crates/originweave-extension/tests/native_messaging_manifest_origin_budget.rs similarity index 97% rename from crates/originweave-core/tests/native_messaging_manifest_origin_budget.rs rename to crates/originweave-extension/tests/native_messaging_manifest_origin_budget.rs index adb57fe3d..66d4f251b 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_origin_budget.rs +++ b/crates/originweave-extension/tests/native_messaging_manifest_origin_budget.rs @@ -1,6 +1,6 @@ #![allow(clippy::expect_used)] -use originweave_core::{ +use originweave_extension::{ NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, }; diff --git a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs b/crates/originweave-extension/tests/native_messaging_manifest_syntax_boundary.rs similarity index 98% rename from crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs rename to crates/originweave-extension/tests/native_messaging_manifest_syntax_boundary.rs index 4fd4d012a..d0e6b08c9 100644 --- a/crates/originweave-core/tests/native_messaging_manifest_syntax_boundary.rs +++ b/crates/originweave-extension/tests/native_messaging_manifest_syntax_boundary.rs @@ -1,6 +1,6 @@ #![allow(clippy::expect_used)] -use originweave_core::{ +use originweave_extension::{ NativeMessagingHostPlatform, NativeMessagingManifestDocument, NativeMessagingManifestParseError, }; From 0da81d96087a6bc814821c3ae68d0cf9fbf0b753 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 03:10:44 +0900 Subject: [PATCH 64/64] fix(extension): restore workspace verification Signed-off-by: Seongho Bae --- Cargo.lock | 2 +- crates/originweave-extension/src/root.rs | 3 +- ...ative_messaging_manifest_document_edges.rs | 119 ++++++++++++++---- tests/test_repository_contract.py | 1 + 4 files changed, 97 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 75671577a..4e01dfe1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -581,7 +581,7 @@ dependencies = [ name = "tinyvec_macros" version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86c61904b462b55fb3df8837a366dfc601a0161d0532f20" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "typenum" diff --git a/crates/originweave-extension/src/root.rs b/crates/originweave-extension/src/root.rs index 7f9cac6a0..a6989ec86 100644 --- a/crates/originweave-extension/src/root.rs +++ b/crates/originweave-extension/src/root.rs @@ -8,7 +8,8 @@ #![deny(missing_docs)] pub use originweave_core::{ - ExtensionId, NativeMessagingAccessRequest, NativeMessagingHostName, NativeMessagingHostNameError, + ExtensionId, NativeMessagingAccessRequest, NativeMessagingHostName, + NativeMessagingHostNameError, }; mod native_messaging_manifest; diff --git a/crates/originweave-extension/tests/native_messaging_manifest_document_edges.rs b/crates/originweave-extension/tests/native_messaging_manifest_document_edges.rs index 08b9af861..773f11337 100644 --- a/crates/originweave-extension/tests/native_messaging_manifest_document_edges.rs +++ b/crates/originweave-extension/tests/native_messaging_manifest_document_edges.rs @@ -19,16 +19,31 @@ fn parse_error(raw: &str) -> NativeMessagingManifestParseError { #[test] fn complete_parser_rejects_every_duplicate_reviewed_field() { let cases = [ - format!(r#"{{"name":"com.contextualwisdom.originweave","name":"com.contextualwisdom.other","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), - format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","description":"other","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), - format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","path":"/tmp/other","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), - format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), - format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"allowed_origins":["{EXTENSION_ORIGIN}"]}}"#), - format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":true,"supports_native_initiated_connections":false}}"#), + format!( + r#"{{"name":"com.contextualwisdom.originweave","name":"com.contextualwisdom.other","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","description":"other","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","path":"/tmp/other","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":true,"supports_native_initiated_connections":false}}"# + ), ]; for raw in cases { - assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::DuplicateField); + assert_eq!( + parse_error(&raw), + NativeMessagingManifestParseError::DuplicateField + ); } } @@ -42,9 +57,14 @@ fn complete_parser_covers_empty_and_malformed_array_and_boolean_shapes() { ); let empty_origins = r#"{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":[]}"#.to_owned(); - assert!(matches!(parse_error(&empty_origins), NativeMessagingManifestParseError::Manifest(_))); + assert!(matches!( + parse_error(&empty_origins), + NativeMessagingManifestParseError::Manifest(_) + )); - let multiple_origins = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}","{SECOND_EXTENSION_ORIGIN}"]}}"#); + let multiple_origins = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}","{SECOND_EXTENSION_ORIGIN}"]}}"# + ); let multiple_manifest = NativeMessagingManifestDocument::parse(multiple_origins.as_bytes()) .expect("valid multi-origin fixture passes pre-parser") .parse_host_manifest(NativeMessagingHostPlatform::Linux) @@ -59,17 +79,31 @@ fn complete_parser_covers_empty_and_malformed_array_and_boolean_shapes() { } for raw in [ - format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}",]}}"#), - format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}" "{EXTENSION_ORIGIN}"]}}"#), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}",]}}"# + ), + format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}" "{EXTENSION_ORIGIN}"]}}"# + ), "{} {}".to_owned(), ] { - assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidJson); + assert_eq!( + parse_error(&raw), + NativeMessagingManifestParseError::InvalidJson + ); } - let invalid_boolean = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":1}}"#); - assert_eq!(parse_error(&invalid_boolean), NativeMessagingManifestParseError::InvalidFieldType); + let invalid_boolean = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":1}}"# + ); + assert_eq!( + parse_error(&invalid_boolean), + NativeMessagingManifestParseError::InvalidFieldType + ); - let false_boolean = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":false}}"#); + let false_boolean = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"],"supports_native_initiated_connections":false}}"# + ); let manifest = NativeMessagingManifestDocument::parse(false_boolean.as_bytes()) .expect("valid false-boolean fixture passes pre-parser") .parse_host_manifest(NativeMessagingHostPlatform::Linux) @@ -79,23 +113,46 @@ fn complete_parser_covers_empty_and_malformed_array_and_boolean_shapes() { #[test] fn complete_parser_covers_json_escape_and_unicode_failure_edges() { - let escaped_description = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"a\b\f\n\r\t-\u0041-\u00E9-\u263A-\uD83D\uDE00-\u00AF","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#); + let escaped_description = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"a\b\f\n\r\t-\u0041-\u00E9-\u263A-\uD83D\uDE00-\u00AF","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ); let manifest = NativeMessagingManifestDocument::parse(escaped_description.as_bytes()) .expect("valid escaped-string fixture passes pre-parser") .parse_host_manifest(NativeMessagingHostPlatform::Linux) .expect("valid escaped-string fixture passes complete parsing"); assert_eq!(manifest.allowed_extension_count(), 1); - for description in [r#"bad\q"#, r#"bad\uD83D"#, r#"bad\uD83D\x0000"#, r#"bad\uD83D\u0041"#, r#"bad\uDE00"#, r#"bad\u12"#, r#"bad\u00G0"#] { - let raw = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"{description}","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#); - assert_eq!(parse_error(&raw), NativeMessagingManifestParseError::InvalidJson); + for description in [ + r#"bad\q"#, + r#"bad\uD83D"#, + r#"bad\uD83D\x0000"#, + r#"bad\uD83D\u0041"#, + r#"bad\uDE00"#, + r#"bad\u12"#, + r#"bad\u00G0"#, + ] { + let raw = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"{description}","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ); + assert_eq!( + parse_error(&raw), + NativeMessagingManifestParseError::InvalidJson + ); } - let raw_control = format!("{{\"name\":\"com.contextualwisdom.originweave\",\"description\":\"bad\u{0001}text\",\"path\":\"/opt/originweave/native-host\",\"type\":\"stdio\",\"allowed_origins\":[\"{EXTENSION_ORIGIN}\"]}}"); - assert_eq!(parse_error(&raw_control), NativeMessagingManifestParseError::InvalidJson); + let raw_control = format!( + "{{\"name\":\"com.contextualwisdom.originweave\",\"description\":\"bad\u{0001}text\",\"path\":\"/opt/originweave/native-host\",\"type\":\"stdio\",\"allowed_origins\":[\"{EXTENSION_ORIGIN}\"]}}" + ); + assert_eq!( + parse_error(&raw_control), + NativeMessagingManifestParseError::InvalidJson + ); let unterminated = r#"{"name":"unterminated}"#; - assert_eq!(parse_error(unterminated), NativeMessagingManifestParseError::InvalidJson); + assert_eq!( + parse_error(unterminated), + NativeMessagingManifestParseError::InvalidJson + ); } #[test] @@ -111,15 +168,25 @@ fn parse_errors_expose_deterministic_display_and_only_causal_sources() { assert!(error.source().is_none()); } - let invalid_host = format!(r#"{{"name":"INVALID HOST","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#); + let invalid_host = format!( + r#"{{"name":"INVALID HOST","description":"host","path":"/opt/originweave/native-host","type":"stdio","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ); let host_error = parse_error(&invalid_host); - assert!(matches!(host_error, NativeMessagingManifestParseError::HostName(_))); + assert!(matches!( + host_error, + NativeMessagingManifestParseError::HostName(_) + )); assert!(!host_error.to_string().is_empty()); assert!(host_error.source().is_some()); - let invalid_manifest = format!(r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"pipe","allowed_origins":["{EXTENSION_ORIGIN}"]}}"#); + let invalid_manifest = format!( + r#"{{"name":"com.contextualwisdom.originweave","description":"host","path":"/opt/originweave/native-host","type":"pipe","allowed_origins":["{EXTENSION_ORIGIN}"]}}"# + ); let manifest_error = parse_error(&invalid_manifest); - assert!(matches!(manifest_error, NativeMessagingManifestParseError::Manifest(_))); + assert!(matches!( + manifest_error, + NativeMessagingManifestParseError::Manifest(_) + )); assert!(!manifest_error.to_string().is_empty()); assert!(manifest_error.source().is_some()); } diff --git a/tests/test_repository_contract.py b/tests/test_repository_contract.py index 057a0011b..0db84ef69 100644 --- a/tests/test_repository_contract.py +++ b/tests/test_repository_contract.py @@ -20,6 +20,7 @@ def test_workspace_declares_all_independently_reusable_crates(self) -> None: set(data["workspace"]["members"]), { "crates/originweave-core", + "crates/originweave-extension", "crates/originweave-bap", "crates/originweave-policy", "crates/originweave-destination",