From 04557ad1f29dad75d065efe481a7acce50e73bdb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 04:03:07 +0900 Subject: [PATCH 01/26] test(mcp): require typed stateless tool routing boundary --- .../tests/mcp_authority_route.rs | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) create mode 100644 crates/originweave-core/tests/mcp_authority_route.rs diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs new file mode 100644 index 000000000..3a5efba9c --- /dev/null +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -0,0 +1,135 @@ +use std::error::Error; + +use originweave_core::ActionKind; +use originweave_core::mcp::{ + MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, + ValidatedMcpToolCall, +}; + +fn validate(tool_name: &str) -> Result { + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + tool_name, + MCP_TOOLS_CALL_METHOD, + tool_name, + ) +} + +#[test] +fn supported_mcp_tools_map_to_exact_originweave_actions() -> Result<(), Box> { + let cases = [ + ("originweave.observe", ActionKind::Observe), + ("originweave.extract", ActionKind::Extract), + ("originweave.navigate", ActionKind::Navigate), + ("originweave.download", ActionKind::Download), + ("originweave.draft", ActionKind::Draft), + ("originweave.submit", ActionKind::Submit), + ("originweave.upload", ActionKind::Upload), + ("originweave.fill_secret", ActionKind::FillSecret), + ("originweave.purchase", ActionKind::Purchase), + ("originweave.delete", ActionKind::Delete), + ("originweave.manage_permission", ActionKind::ManagePermission), + ]; + + for (tool_name, expected_action) in cases { + let call = validate(tool_name)?; + assert_eq!(call.tool_name(), tool_name); + assert_eq!(call.action_kind(), expected_action); + assert_eq!(call.action_kind().required_capability(), expected_action.required_capability()); + assert_eq!(call.action_kind().risk_class(), expected_action.risk_class()); + } + Ok(()) +} + +#[test] +fn mcp_route_rejects_protocol_header_body_and_method_drift() { + assert_eq!( + ValidatedMcpToolCall::new( + "2025-11-25", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::UnsupportedProtocolVersion) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + "tools/list", + "originweave.observe", + ), + Err(McpToolBoundaryError::HeaderBodyMismatch) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.extract", + ), + Err(McpToolBoundaryError::HeaderBodyMismatch) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + "resources/read", + "originweave.observe", + "resources/read", + "originweave.observe", + ), + Err(McpToolBoundaryError::UnsupportedMethod) + ); +} + +#[test] +fn mcp_route_rejects_unbounded_malformed_and_unmapped_tool_names() { + let oversized = "x".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + for tool_name in ["", "originweave legal", "originweave/observe", "originweave.관찰", &oversized] { + assert_eq!(validate(tool_name), Err(McpToolBoundaryError::InvalidToolName)); + } + + assert_eq!( + validate("originweave.legal_consent"), + Err(McpToolBoundaryError::UnknownTool) + ); + assert_eq!( + validate("third_party.arbitrary_javascript"), + Err(McpToolBoundaryError::UnknownTool) + ); +} + +#[test] +fn mcp_boundary_errors_are_deterministic_and_do_not_echo_untrusted_values() { + let cases = [ + ( + McpToolBoundaryError::UnsupportedProtocolVersion, + "unsupported MCP protocol version", + ), + ( + McpToolBoundaryError::HeaderBodyMismatch, + "MCP routing headers do not match the request body", + ), + ( + McpToolBoundaryError::UnsupportedMethod, + "only MCP tools/call requests can enter the typed action boundary", + ), + ( + McpToolBoundaryError::InvalidToolName, + "MCP tool name violates the bounded ASCII routing syntax", + ), + ( + McpToolBoundaryError::UnknownTool, + "MCP tool is not mapped to an OriginWeave typed action", + ), + ]; + + for (error, expected_message) in cases { + assert_eq!(error.to_string(), expected_message); + assert!(error.source().is_none()); + } +} From 34311626859ec335af003c75d2de3aed959278a0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 04:05:44 +0900 Subject: [PATCH 02/26] style(mcp): apply canonical rustfmt to red contract --- .../tests/mcp_authority_route.rs | 28 +++++++++++++++---- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs index 3a5efba9c..5bda207e7 100644 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -29,15 +29,24 @@ fn supported_mcp_tools_map_to_exact_originweave_actions() -> Result<(), Box Date: Sun, 16 Aug 2026 04:11:37 +0900 Subject: [PATCH 03/26] feat(mcp): enforce typed stateless routing boundary --- crates/originweave-core/Cargo.toml | 3 + crates/originweave-core/src/mcp.rs | 143 ++++++++++++++++++++++++++++ crates/originweave-core/src/root.rs | 15 +++ 3 files changed, 161 insertions(+) create mode 100644 crates/originweave-core/src/mcp.rs create mode 100644 crates/originweave-core/src/root.rs 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] diff --git a/crates/originweave-core/src/mcp.rs b/crates/originweave-core/src/mcp.rs new file mode 100644 index 000000000..d98cc1323 --- /dev/null +++ b/crates/originweave-core/src/mcp.rs @@ -0,0 +1,143 @@ +//! Fail-closed MCP routing integrity for the external adapter boundary. +//! +//! This module validates only the stateless MCP protocol/method/tool routing +//! envelope and derives an existing [`ActionKind`]. It is deliberately not an +//! authorization decision: callers must independently enforce OriginWeave +//! capability, risk, approval, origin, secret-broker, and evidence policies. +//! No MCP arguments, outputs, credentials, or arbitrary model-visible values +//! are retained by this boundary. + +use std::fmt; + +use crate::ActionKind; + +/// MCP protocol generation accepted by this stateless adapter boundary. +pub const MCP_PROTOCOL_VERSION: &str = "2026-07-28"; + +/// The only MCP method that can enter the typed action-routing boundary. +pub const MCP_TOOLS_CALL_METHOD: &str = "tools/call"; + +/// Maximum accepted MCP tool-name length in bytes. +pub const MAX_MCP_TOOL_NAME_BYTES: usize = 128; + +/// A deterministic failure while validating untrusted MCP routing metadata. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum McpToolBoundaryError { + /// The request names an MCP protocol generation this boundary does not support. + UnsupportedProtocolVersion, + /// MCP routing metadata disagrees with the method or tool name in the body. + HeaderBodyMismatch, + /// The request method is not the supported `tools/call` operation. + UnsupportedMethod, + /// The tool name violates the bounded ASCII MCP routing syntax. + InvalidToolName, + /// The tool name has no explicit mapping to an OriginWeave typed action. + UnknownTool, +} + +impl fmt::Display for McpToolBoundaryError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::UnsupportedProtocolVersion => { + formatter.write_str("unsupported MCP protocol version") + } + Self::HeaderBodyMismatch => { + formatter.write_str("MCP routing headers do not match the request body") + } + Self::UnsupportedMethod => formatter + .write_str("only MCP tools/call requests can enter the typed action boundary"), + Self::InvalidToolName => { + formatter.write_str("MCP tool name violates the bounded ASCII routing syntax") + } + Self::UnknownTool => { + formatter.write_str("MCP tool is not mapped to an OriginWeave typed action") + } + } + } +} + +impl std::error::Error for McpToolBoundaryError {} + +/// An MCP tool call whose routing envelope has been validated and mapped. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ValidatedMcpToolCall { + tool_name: &'static str, + action_kind: ActionKind, +} + +impl ValidatedMcpToolCall { + /// Validate one stateless MCP tool-call routing envelope. + /// + /// Routing integrity is intentionally narrower than authorization. A + /// successful value proves only that the untrusted protocol version, + /// routing metadata, body method, and body tool name agree with one + /// explicitly supported mapping. + pub fn new( + protocol_version: &str, + routing_method: &str, + routing_tool_name: &str, + body_method: &str, + body_tool_name: &str, + ) -> Result { + if protocol_version != MCP_PROTOCOL_VERSION { + return Err(McpToolBoundaryError::UnsupportedProtocolVersion); + } + if routing_method != body_method || routing_tool_name != body_tool_name { + return Err(McpToolBoundaryError::HeaderBodyMismatch); + } + if routing_method != MCP_TOOLS_CALL_METHOD { + return Err(McpToolBoundaryError::UnsupportedMethod); + } + if !valid_tool_name(routing_tool_name) { + return Err(McpToolBoundaryError::InvalidToolName); + } + + let (tool_name, action_kind) = map_tool(routing_tool_name)?; + Ok(Self { + tool_name, + action_kind, + }) + } + + /// Return the canonical static tool name selected by the explicit mapping. + #[must_use] + pub const fn tool_name(&self) -> &'static str { + self.tool_name + } + + /// Return the existing OriginWeave typed action selected by this tool. + #[must_use] + pub const fn action_kind(&self) -> ActionKind { + self.action_kind + } +} + +fn valid_tool_name(tool_name: &str) -> bool { + if tool_name.is_empty() || tool_name.len() > MAX_MCP_TOOL_NAME_BYTES { + return false; + } + tool_name.bytes().all(|byte| { + byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.') + }) +} + +fn map_tool(tool_name: &str) -> Result<(&'static str, ActionKind), McpToolBoundaryError> { + let mapped = match tool_name { + "originweave.observe" => ("originweave.observe", ActionKind::Observe), + "originweave.extract" => ("originweave.extract", ActionKind::Extract), + "originweave.navigate" => ("originweave.navigate", ActionKind::Navigate), + "originweave.download" => ("originweave.download", ActionKind::Download), + "originweave.draft" => ("originweave.draft", ActionKind::Draft), + "originweave.submit" => ("originweave.submit", ActionKind::Submit), + "originweave.upload" => ("originweave.upload", ActionKind::Upload), + "originweave.fill_secret" => ("originweave.fill_secret", ActionKind::FillSecret), + "originweave.purchase" => ("originweave.purchase", ActionKind::Purchase), + "originweave.delete" => ("originweave.delete", ActionKind::Delete), + "originweave.manage_permission" => ( + "originweave.manage_permission", + ActionKind::ManagePermission, + ), + _ => return Err(McpToolBoundaryError::UnknownTool), + }; + Ok(mapped) +} diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs new file mode 100644 index 000000000..7acced460 --- /dev/null +++ b/crates/originweave-core/src/root.rs @@ -0,0 +1,15 @@ +//! Shared security and governance contracts for OriginWeave. +//! +//! The historical core contracts remain source-compatible while adapter-specific +//! boundaries can live in focused modules without changing their authority model. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +#[path = "lib.rs"] +mod contracts; + +pub use contracts::*; + +/// Stateless MCP routing validation that maps only explicit tools to typed actions. +pub mod mcp; From af308023b0d530dd7625cce764f936a01d37ca81 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 04:13:46 +0900 Subject: [PATCH 04/26] style(mcp): apply canonical production rustfmt --- crates/originweave-core/src/mcp.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/mcp.rs b/crates/originweave-core/src/mcp.rs index d98cc1323..a83facb88 100644 --- a/crates/originweave-core/src/mcp.rs +++ b/crates/originweave-core/src/mcp.rs @@ -116,9 +116,9 @@ fn valid_tool_name(tool_name: &str) -> bool { if tool_name.is_empty() || tool_name.len() > MAX_MCP_TOOL_NAME_BYTES { return false; } - tool_name.bytes().all(|byte| { - byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.') - }) + tool_name + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.')) } fn map_tool(tool_name: &str) -> Result<(&'static str, ActionKind), McpToolBoundaryError> { From 3ac6855a47cbbb5a10c0597798295b53fd52d529 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 06:34:26 +0900 Subject: [PATCH 05/26] test(mcp): pin routing policy boundaries --- .../tests/mcp_authority_route.rs | 91 +++++++++++++++---- 1 file changed, 71 insertions(+), 20 deletions(-) diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs index 5bda207e7..bb4641234 100644 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -1,10 +1,10 @@ use std::error::Error; -use originweave_core::ActionKind; use originweave_core::mcp::{ MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, ValidatedMcpToolCall, }; +use originweave_core::{ActionKind, Capability, RiskClass}; fn validate(tool_name: &str) -> Result { ValidatedMcpToolCall::new( @@ -19,34 +19,80 @@ fn validate(tool_name: &str) -> Result Result<(), Box> { let cases = [ - ("originweave.observe", ActionKind::Observe), - ("originweave.extract", ActionKind::Extract), - ("originweave.navigate", ActionKind::Navigate), - ("originweave.download", ActionKind::Download), - ("originweave.draft", ActionKind::Draft), - ("originweave.submit", ActionKind::Submit), - ("originweave.upload", ActionKind::Upload), - ("originweave.fill_secret", ActionKind::FillSecret), - ("originweave.purchase", ActionKind::Purchase), - ("originweave.delete", ActionKind::Delete), + ( + "originweave.observe", + ActionKind::Observe, + Capability::Observe, + RiskClass::R0, + ), + ( + "originweave.extract", + ActionKind::Extract, + Capability::Extract, + RiskClass::R0, + ), + ( + "originweave.navigate", + ActionKind::Navigate, + Capability::Navigate, + RiskClass::R1, + ), + ( + "originweave.download", + ActionKind::Download, + Capability::Download, + RiskClass::R1, + ), + ( + "originweave.draft", + ActionKind::Draft, + Capability::Draft, + RiskClass::R2, + ), + ( + "originweave.submit", + ActionKind::Submit, + Capability::Submit, + RiskClass::R3, + ), + ( + "originweave.upload", + ActionKind::Upload, + Capability::Upload, + RiskClass::R3, + ), + ( + "originweave.fill_secret", + ActionKind::FillSecret, + Capability::FillSecret, + RiskClass::R3, + ), + ( + "originweave.purchase", + ActionKind::Purchase, + Capability::Purchase, + RiskClass::R4, + ), + ( + "originweave.delete", + ActionKind::Delete, + Capability::Delete, + RiskClass::R4, + ), ( "originweave.manage_permission", ActionKind::ManagePermission, + Capability::ManagePermission, + RiskClass::R4, ), ]; - for (tool_name, expected_action) in cases { + for (tool_name, expected_action, expected_capability, expected_risk) in cases { let call = validate(tool_name)?; assert_eq!(call.tool_name(), tool_name); assert_eq!(call.action_kind(), expected_action); - assert_eq!( - call.action_kind().required_capability(), - expected_action.required_capability() - ); - assert_eq!( - call.action_kind().risk_class(), - expected_action.risk_class() - ); + assert_eq!(call.action_kind().required_capability(), expected_capability); + assert_eq!(call.action_kind().risk_class(), expected_risk); } Ok(()) } @@ -97,6 +143,7 @@ fn mcp_route_rejects_protocol_header_body_and_method_drift() { #[test] fn mcp_route_rejects_unbounded_malformed_and_unmapped_tool_names() { + let at_limit = "x".repeat(MAX_MCP_TOOL_NAME_BYTES); let oversized = "x".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); for tool_name in [ "", @@ -111,6 +158,10 @@ fn mcp_route_rejects_unbounded_malformed_and_unmapped_tool_names() { ); } + assert_eq!( + validate(&at_limit), + Err(McpToolBoundaryError::UnknownTool) + ); assert_eq!( validate("originweave.legal_consent"), Err(McpToolBoundaryError::UnknownTool) From bfed566e556f2c694ed639cbdebbf9e733c37a45 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 06:36:06 +0900 Subject: [PATCH 06/26] refactor(mcp): centralize explicit tool mapping --- crates/originweave-core/src/mcp.rs | 41 +++++++++++++++++------------- 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/crates/originweave-core/src/mcp.rs b/crates/originweave-core/src/mcp.rs index a83facb88..297d050c1 100644 --- a/crates/originweave-core/src/mcp.rs +++ b/crates/originweave-core/src/mcp.rs @@ -20,6 +20,24 @@ pub const MCP_TOOLS_CALL_METHOD: &str = "tools/call"; /// Maximum accepted MCP tool-name length in bytes. pub const MAX_MCP_TOOL_NAME_BYTES: usize = 128; +/// The complete explicit MCP tool-to-action mapping accepted by this boundary. +const MCP_TOOL_ACTION_MAP: &[(&str, ActionKind)] = &[ + ("originweave.observe", ActionKind::Observe), + ("originweave.extract", ActionKind::Extract), + ("originweave.navigate", ActionKind::Navigate), + ("originweave.download", ActionKind::Download), + ("originweave.draft", ActionKind::Draft), + ("originweave.submit", ActionKind::Submit), + ("originweave.upload", ActionKind::Upload), + ("originweave.fill_secret", ActionKind::FillSecret), + ("originweave.purchase", ActionKind::Purchase), + ("originweave.delete", ActionKind::Delete), + ( + "originweave.manage_permission", + ActionKind::ManagePermission, + ), +]; + /// A deterministic failure while validating untrusted MCP routing metadata. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum McpToolBoundaryError { @@ -122,22 +140,9 @@ fn valid_tool_name(tool_name: &str) -> bool { } fn map_tool(tool_name: &str) -> Result<(&'static str, ActionKind), McpToolBoundaryError> { - let mapped = match tool_name { - "originweave.observe" => ("originweave.observe", ActionKind::Observe), - "originweave.extract" => ("originweave.extract", ActionKind::Extract), - "originweave.navigate" => ("originweave.navigate", ActionKind::Navigate), - "originweave.download" => ("originweave.download", ActionKind::Download), - "originweave.draft" => ("originweave.draft", ActionKind::Draft), - "originweave.submit" => ("originweave.submit", ActionKind::Submit), - "originweave.upload" => ("originweave.upload", ActionKind::Upload), - "originweave.fill_secret" => ("originweave.fill_secret", ActionKind::FillSecret), - "originweave.purchase" => ("originweave.purchase", ActionKind::Purchase), - "originweave.delete" => ("originweave.delete", ActionKind::Delete), - "originweave.manage_permission" => ( - "originweave.manage_permission", - ActionKind::ManagePermission, - ), - _ => return Err(McpToolBoundaryError::UnknownTool), - }; - Ok(mapped) + MCP_TOOL_ACTION_MAP + .iter() + .copied() + .find(|(mapped_name, _)| *mapped_name == tool_name) + .ok_or(McpToolBoundaryError::UnknownTool) } From e5ac16d272225268fc913eded30dbc62c8796065 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 06:40:17 +0900 Subject: [PATCH 07/26] style(mcp): apply canonical rustfmt diagnostics --- crates/originweave-core/tests/mcp_authority_route.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs index bb4641234..c5fc81990 100644 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -91,7 +91,10 @@ fn supported_mcp_tools_map_to_exact_originweave_actions() -> Result<(), Box Date: Sun, 16 Aug 2026 09:01:43 +0900 Subject: [PATCH 08/26] test(mcp): bind routed tool identity to policy action --- .../tests/mcp_route_binding.rs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 crates/originweave-policy/tests/mcp_route_binding.rs diff --git a/crates/originweave-policy/tests/mcp_route_binding.rs b/crates/originweave-policy/tests/mcp_route_binding.rs new file mode 100644 index 000000000..4079471d7 --- /dev/null +++ b/crates/originweave-policy/tests/mcp_route_binding.rs @@ -0,0 +1,98 @@ +#![allow(clippy::expect_used)] + +use std::collections::BTreeSet; + +use originweave_core::mcp::{ + MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall, +}; +use originweave_core::{ + ActionIntentDigest, ActionKind, ActionRequest, ApprovalEvidence, Capability, ExecutionPurpose, + InstructionSource, Origin, PolicyContext, RobotsDecision, SecretDelivery, SessionMode, +}; +use originweave_policy::{Decision, DenialReason, evaluate_mcp}; + +const VALID_INTENT: &str = + "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef"; + +fn origin() -> Origin { + Origin::parse("https://mcp.example").expect("valid test origin") +} + +fn intent() -> ActionIntentDigest { + ActionIntentDigest::parse(VALID_INTENT).expect("valid intent digest") +} + +fn validated_call(tool_name: &str) -> ValidatedMcpToolCall { + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + tool_name, + MCP_TOOLS_CALL_METHOD, + tool_name, + ) + .expect("known test MCP tool") +} + +fn request(action: ActionKind) -> ActionRequest { + let site = origin(); + ActionRequest::new( + action, + site.clone(), + site, + InstructionSource::User, + SecretDelivery::None, + intent(), + ) +} + +fn context(capabilities: BTreeSet) -> PolicyContext { + let site = origin(); + PolicyContext::new( + SessionMode::AgentTask, + ExecutionPurpose::UserDelegatedTask, + capabilities, + BTreeSet::from([site.clone()]), + BTreeSet::from([site]), + RobotsDecision::Allowed, + ApprovalEvidence::None, + ) +} + +#[test] +fn matching_mcp_route_enters_the_existing_policy_boundary() { + let call = validated_call("originweave.observe"); + let decision = evaluate_mcp( + &call, + &request(ActionKind::Observe), + &context(BTreeSet::from([Capability::Observe])), + ); + + assert_eq!(decision, Decision::Allow); +} + +#[test] +fn mismatched_mcp_route_cannot_be_reinterpreted_as_another_action() { + let call = validated_call("originweave.observe"); + let decision = evaluate_mcp( + &call, + &request(ActionKind::Navigate), + &context(BTreeSet::from([Capability::Navigate])), + ); + + assert_eq!(decision, Decision::Deny(DenialReason::McpActionMismatch)); +} + +#[test] +fn matching_mcp_route_does_not_bypass_existing_policy_denials() { + let call = validated_call("originweave.navigate"); + let decision = evaluate_mcp( + &call, + &request(ActionKind::Navigate), + &context(BTreeSet::from([Capability::Observe])), + ); + + assert_eq!( + decision, + Decision::Deny(DenialReason::MissingCapability(Capability::Navigate)) + ); +} From 0808e531fb6546740983cb91e9c28f10293caae5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 09:05:10 +0900 Subject: [PATCH 09/26] feat(mcp): fail closed on route-action mismatch --- crates/originweave-policy/src/lib.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 243ae8ce7..dbfb3c16d 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -15,6 +15,7 @@ pub use sensitive_data::{ evaluate_handle_use, }; +use originweave_core::mcp::ValidatedMcpToolCall; use originweave_core::{ ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode, @@ -40,6 +41,8 @@ pub enum DenialReason { ModePurposeMismatch, /// Page or document content attempted to become a trusted instruction. UntrustedInstructionSource, + /// The validated MCP route resolved to a different action than the policy request. + McpActionMismatch, /// The session lacks the exact capability required by the action. MissingCapability(Capability), /// The target origin is outside the session's read grant. @@ -66,6 +69,23 @@ pub enum DenialReason { ApprovalScopeMismatch, } +/// Evaluate a policy request only when it matches an already validated MCP route. +/// +/// Matching routing metadata grants no authority. Once route and request action agree, the request +/// still passes through the existing action policy unchanged. +#[must_use] +pub fn evaluate_mcp( + call: &ValidatedMcpToolCall, + request: &ActionRequest, + context: &PolicyContext, +) -> Decision { + if call.action_kind() != request.action() { + return Decision::Deny(DenialReason::McpActionMismatch); + } + + evaluate(request, context) +} + /// Evaluate a typed browser action against one explicit policy context. #[must_use] pub fn evaluate(request: &ActionRequest, context: &PolicyContext) -> Decision { From 50c80abe81cf8b68d2f0f20ede9d7a24aa3f60ab Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 09:08:28 +0900 Subject: [PATCH 10/26] style(mcp): apply canonical rustfmt diagnostics --- crates/originweave-policy/tests/mcp_route_binding.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-policy/tests/mcp_route_binding.rs b/crates/originweave-policy/tests/mcp_route_binding.rs index 4079471d7..8e9661af6 100644 --- a/crates/originweave-policy/tests/mcp_route_binding.rs +++ b/crates/originweave-policy/tests/mcp_route_binding.rs @@ -2,9 +2,7 @@ use std::collections::BTreeSet; -use originweave_core::mcp::{ - MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall, -}; +use originweave_core::mcp::{MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall}; use originweave_core::{ ActionIntentDigest, ActionKind, ActionRequest, ApprovalEvidence, Capability, ExecutionPurpose, InstructionSource, Origin, PolicyContext, RobotsDecision, SecretDelivery, SessionMode, From 169814e1327de370f2ac234ea745fdc2ffb40a92 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:03:58 +0900 Subject: [PATCH 11/26] test(mcp): require deterministic tool catalog --- .../tests/mcp_authority_route.rs | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs index c5fc81990..8662a89f9 100644 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -2,7 +2,7 @@ use std::error::Error; use originweave_core::mcp::{ MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, - ValidatedMcpToolCall, + ValidatedMcpToolCall, supported_mcp_tools, }; use originweave_core::{ActionKind, Capability, RiskClass}; @@ -100,6 +100,51 @@ fn supported_mcp_tools_map_to_exact_originweave_actions() -> Result<(), Box Result<(), Box> { + let expected = [ + ("originweave.observe", ActionKind::Observe), + ("originweave.extract", ActionKind::Extract), + ("originweave.navigate", ActionKind::Navigate), + ("originweave.download", ActionKind::Download), + ("originweave.draft", ActionKind::Draft), + ("originweave.submit", ActionKind::Submit), + ("originweave.upload", ActionKind::Upload), + ("originweave.fill_secret", ActionKind::FillSecret), + ("originweave.purchase", ActionKind::Purchase), + ("originweave.delete", ActionKind::Delete), + ("originweave.manage_permission", ActionKind::ManagePermission), + ]; + let catalog = supported_mcp_tools(); + + assert_eq!(catalog.len(), expected.len()); + for (entry, (expected_name, expected_action)) in catalog.iter().zip(expected) { + assert_eq!(entry.tool_name(), expected_name); + assert_eq!(entry.action_kind(), expected_action); + assert_eq!( + entry.required_capability(), + expected_action.required_capability() + ); + assert_eq!(entry.risk_class(), expected_action.risk_class()); + + let call = validate(entry.tool_name())?; + assert_eq!(call.action_kind(), entry.action_kind()); + } + + for (index, entry) in catalog.iter().enumerate() { + for other in &catalog[index + 1..] { + assert_ne!(entry.tool_name(), other.tool_name()); + assert_ne!(entry.action_kind(), other.action_kind()); + } + } + assert!( + catalog + .iter() + .all(|entry| entry.action_kind() != ActionKind::LegalConsent) + ); + Ok(()) +} + #[test] fn mcp_route_rejects_protocol_header_body_and_method_drift() { assert_eq!( From b0a0b307833a3b8b867461a9cdd935afb952185a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:05:48 +0900 Subject: [PATCH 12/26] test(mcp): apply canonical catalog formatting --- crates/originweave-core/tests/mcp_authority_route.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs index 8662a89f9..df359ccf0 100644 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -101,7 +101,8 @@ fn supported_mcp_tools_map_to_exact_originweave_actions() -> Result<(), Box Result<(), Box> { +fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> Result<(), Box> +{ let expected = [ ("originweave.observe", ActionKind::Observe), ("originweave.extract", ActionKind::Extract), @@ -113,7 +114,10 @@ fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> Result ("originweave.fill_secret", ActionKind::FillSecret), ("originweave.purchase", ActionKind::Purchase), ("originweave.delete", ActionKind::Delete), - ("originweave.manage_permission", ActionKind::ManagePermission), + ( + "originweave.manage_permission", + ActionKind::ManagePermission, + ), ]; let catalog = supported_mcp_tools(); From 23a17b556ffafeec59df56657f60c5309ad2b963 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:07:22 +0900 Subject: [PATCH 13/26] feat(mcp): expose deterministic reviewed tool catalog --- crates/originweave-core/src/mcp.rs | 118 ++++++++++++++++++++++++----- 1 file changed, 98 insertions(+), 20 deletions(-) diff --git a/crates/originweave-core/src/mcp.rs b/crates/originweave-core/src/mcp.rs index 297d050c1..c3ef61e15 100644 --- a/crates/originweave-core/src/mcp.rs +++ b/crates/originweave-core/src/mcp.rs @@ -9,7 +9,7 @@ use std::fmt; -use crate::ActionKind; +use crate::{ActionKind, Capability, RiskClass}; /// MCP protocol generation accepted by this stateless adapter boundary. pub const MCP_PROTOCOL_VERSION: &str = "2026-07-28"; @@ -20,24 +20,102 @@ pub const MCP_TOOLS_CALL_METHOD: &str = "tools/call"; /// Maximum accepted MCP tool-name length in bytes. pub const MAX_MCP_TOOL_NAME_BYTES: usize = 128; -/// The complete explicit MCP tool-to-action mapping accepted by this boundary. -const MCP_TOOL_ACTION_MAP: &[(&str, ActionKind)] = &[ - ("originweave.observe", ActionKind::Observe), - ("originweave.extract", ActionKind::Extract), - ("originweave.navigate", ActionKind::Navigate), - ("originweave.download", ActionKind::Download), - ("originweave.draft", ActionKind::Draft), - ("originweave.submit", ActionKind::Submit), - ("originweave.upload", ActionKind::Upload), - ("originweave.fill_secret", ActionKind::FillSecret), - ("originweave.purchase", ActionKind::Purchase), - ("originweave.delete", ActionKind::Delete), - ( - "originweave.manage_permission", - ActionKind::ManagePermission, - ), +/// One deterministic MCP tool descriptor derived from OriginWeave's reviewed action registry. +/// +/// The descriptor is discovery metadata only. It does not grant capabilities, origin access, +/// approval, secret access, or any other authority. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct McpToolCatalogEntry { + tool_name: &'static str, + action_kind: ActionKind, +} + +impl McpToolCatalogEntry { + /// Return the canonical MCP tool name exposed by this registry entry. + #[must_use] + pub const fn tool_name(&self) -> &'static str { + self.tool_name + } + + /// Return the typed OriginWeave action represented by this registry entry. + #[must_use] + pub const fn action_kind(&self) -> ActionKind { + self.action_kind + } + + /// Return the capability required by the represented action. + #[must_use] + pub const fn required_capability(&self) -> Capability { + self.action_kind.required_capability() + } + + /// Return the risk class assigned to the represented action. + #[must_use] + pub const fn risk_class(&self) -> RiskClass { + self.action_kind.risk_class() + } +} + +/// The complete explicit MCP tool-to-action registry accepted by this boundary. +/// +/// Order is deterministic so adapters can derive stable discovery output from this single +/// reviewed registry rather than maintaining a second mapping that could drift from routing. +const MCP_TOOL_CATALOG: &[McpToolCatalogEntry] = &[ + McpToolCatalogEntry { + tool_name: "originweave.observe", + action_kind: ActionKind::Observe, + }, + McpToolCatalogEntry { + tool_name: "originweave.extract", + action_kind: ActionKind::Extract, + }, + McpToolCatalogEntry { + tool_name: "originweave.navigate", + action_kind: ActionKind::Navigate, + }, + McpToolCatalogEntry { + tool_name: "originweave.download", + action_kind: ActionKind::Download, + }, + McpToolCatalogEntry { + tool_name: "originweave.draft", + action_kind: ActionKind::Draft, + }, + McpToolCatalogEntry { + tool_name: "originweave.submit", + action_kind: ActionKind::Submit, + }, + McpToolCatalogEntry { + tool_name: "originweave.upload", + action_kind: ActionKind::Upload, + }, + McpToolCatalogEntry { + tool_name: "originweave.fill_secret", + action_kind: ActionKind::FillSecret, + }, + McpToolCatalogEntry { + tool_name: "originweave.purchase", + action_kind: ActionKind::Purchase, + }, + McpToolCatalogEntry { + tool_name: "originweave.delete", + action_kind: ActionKind::Delete, + }, + McpToolCatalogEntry { + tool_name: "originweave.manage_permission", + action_kind: ActionKind::ManagePermission, + }, ]; +/// Return the deterministic reviewed MCP tool catalog. +/// +/// Adapters may use this slice to derive discovery responses. Serialization, pagination, cache +/// policy, transport I/O, and authorization remain outside this stateless registry boundary. +#[must_use] +pub const fn supported_mcp_tools() -> &'static [McpToolCatalogEntry] { + MCP_TOOL_CATALOG +} + /// A deterministic failure while validating untrusted MCP routing metadata. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum McpToolBoundaryError { @@ -140,9 +218,9 @@ fn valid_tool_name(tool_name: &str) -> bool { } fn map_tool(tool_name: &str) -> Result<(&'static str, ActionKind), McpToolBoundaryError> { - MCP_TOOL_ACTION_MAP + MCP_TOOL_CATALOG .iter() - .copied() - .find(|(mapped_name, _)| *mapped_name == tool_name) + .find(|entry| entry.tool_name == tool_name) + .map(|entry| (entry.tool_name, entry.action_kind)) .ok_or(McpToolBoundaryError::UnknownTool) } From 9520ffeca808bf75a17c059047534ae494691815 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sun, 16 Aug 2026 13:10:28 +0900 Subject: [PATCH 14/26] docs(changelog): record deterministic MCP tool catalog --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6e4bd39ca..7b3b8edfb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,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. - 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 MCP 2026-07-28 stateless tool routing with bounded names, a single reviewed tool-to-action registry shared by routing and deterministic adapter discovery, and fail-closed policy binding that grants no ambient authority. - 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 1d86d28c5e44fac228807446ee6046d9e5982a42 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 17:12:25 +0900 Subject: [PATCH 15/26] test(mcp): reject malformed cross-field tool names before mismatch --- .../tests/mcp_authority_route.rs | 37 +++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs index df359ccf0..a1b1b6fc3 100644 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -221,6 +221,43 @@ fn mcp_route_rejects_unbounded_malformed_and_unmapped_tool_names() { ); } +#[test] +fn mcp_route_validates_each_untrusted_tool_name_before_cross_field_comparison() { + let oversized_routing = "r".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + let oversized_body = "b".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + &oversized_routing, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidToolName) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + &oversized_body, + ), + Err(McpToolBoundaryError::InvalidToolName) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave/observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidToolName) + ); +} + #[test] fn mcp_boundary_errors_are_deterministic_and_do_not_echo_untrusted_values() { let cases = [ From 9d0f524e47b37dd92971c094893a68b9f7307eff Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 17:14:14 +0900 Subject: [PATCH 16/26] fix(mcp): validate each untrusted tool name before comparison --- crates/originweave-core/src/mcp.rs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/crates/originweave-core/src/mcp.rs b/crates/originweave-core/src/mcp.rs index c3ef61e15..dd8d60258 100644 --- a/crates/originweave-core/src/mcp.rs +++ b/crates/originweave-core/src/mcp.rs @@ -167,7 +167,9 @@ impl ValidatedMcpToolCall { /// Routing integrity is intentionally narrower than authorization. A /// successful value proves only that the untrusted protocol version, /// routing metadata, body method, and body tool name agree with one - /// explicitly supported mapping. + /// explicitly supported mapping. Each untrusted tool name is shape-validated + /// before cross-field comparison so malformed or oversized names cannot + /// bypass the bounded routing syntax through mismatch handling. pub fn new( protocol_version: &str, routing_method: &str, @@ -178,15 +180,15 @@ impl ValidatedMcpToolCall { if protocol_version != MCP_PROTOCOL_VERSION { return Err(McpToolBoundaryError::UnsupportedProtocolVersion); } + if !valid_tool_name(routing_tool_name) || !valid_tool_name(body_tool_name) { + return Err(McpToolBoundaryError::InvalidToolName); + } if routing_method != body_method || routing_tool_name != body_tool_name { return Err(McpToolBoundaryError::HeaderBodyMismatch); } if routing_method != MCP_TOOLS_CALL_METHOD { return Err(McpToolBoundaryError::UnsupportedMethod); } - if !valid_tool_name(routing_tool_name) { - return Err(McpToolBoundaryError::InvalidToolName); - } let (tool_name, action_kind) = map_tool(routing_tool_name)?; Ok(Self { From 2fb12da27991e01764fb95df5a207f86927923e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 18:38:01 +0900 Subject: [PATCH 17/26] docs(mcp): distinguish active routing from shipped adapter --- CHANGELOG.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 91cb1666c..965d4a2d9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,13 +11,13 @@ 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. - 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 MCP 2026-07-28 stateless tool routing with bounded names, a single reviewed tool-to-action registry shared by routing and deterministic adapter discovery, and fail-closed policy binding that grants no ambient authority. +- Active PR #168 adds 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. This is active-PR evidence only; the complete MCP adapter, transport serialization, discovery response handling, OAuth, browser I/O, and persistence remain planned until separately integrated on protected `main`. - 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. - Authenticated `originweave-tls` service-identity boundary that consumes an existing verified TCP stream, requires exact TLS-origin and transport-origin equality, derives RFC 9525 DNS or literal-IP reference identity only from the canonical HTTPS origin, validates WebPKI with explicit roots and fixed time, permits only TLS 1.2 and TLS 1.3, and never reconnects or resolves. - Bounded TLS policy for total handshake time, ALPN identifiers, trust-root count and bytes, and server-presented certificate count and bytes, with explicit optional-versus-required ALPN behavior and `NotConfigured` revocation evidence. -- Credential-free TLS evidence containing canonical origin, requested and observed peer, DNS/IP reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. +- Credential-free TLS evidence containing canonical origin, TCP peers, reference identity, TLS version, cipher-suite identifier, selected ALPN or explicit absence, leaf certificate and SPKI hashes, server-presented certificate hashes and bounds, trust-bundle identity and hash, validity interval, fixed verification time, revocation configuration, and measured handshake duration. - Credential-free connection and redirect evidence containing canonical addresses, destination classes, target digests, hop numbers, and approved-address counts. - Credential-free verified TCP evidence containing the logical origin, requested socket, observed peer, destination class, successful attempt number, and per-attempt timeout. - Standard `Display` and `std::error::Error` contracts for destination, redirect, digest, direct-network, and TLS failures, including preserved destination-policy, rustls, and operating-system sources where applicable. From 9bc35d1314bbfbcf98368de1a08ba8f55668a212 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 18:38:29 +0900 Subject: [PATCH 18/26] docs(mcp): record active routing authority boundary --- .../0107-browser-protocol-adapter-strategy.md | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/docs/adr/0107-browser-protocol-adapter-strategy.md b/docs/adr/0107-browser-protocol-adapter-strategy.md index 8923616be..1c71105d2 100644 --- a/docs/adr/0107-browser-protocol-adapter-strategy.md +++ b/docs/adr/0107-browser-protocol-adapter-strategy.md @@ -34,6 +34,14 @@ OriginWeave exposes its own versioned protocol for session, observation, query, MCP version negotiation is independent of the OriginWeave Protocol version. As of this review, MCP `2026-07-28` is the current released protocol generation; a future MCP change does not silently alter OriginWeave task, approval, secret, tenant, or browser semantics. MCP tool/resource content remains untrusted input and any server-to-client/user interaction capability is mediated by the same OriginWeave policy/approval boundaries as other adapter traffic. +### Current implementation boundary + +The complete MCP adapter remains **Planned**. Active PR #168 is narrower **IMPLEMENTED_ON_ACTIVE_PR** evidence inside the Rust control plane: it validates the `2026-07-28` stateless `tools/call` routing envelope presented to this boundary, bounds and syntax-checks both untrusted tool-name fields, derives one of the existing typed `ActionKind` values from a deterministic reviewed registry, exposes discovery metadata from that same registry, and requires the resulting action to pass the ordinary OriginWeave policy evaluator. The catalog and validated route grant no capability, approval, origin, secret, browser, persistence, or evidence authority by themselves. + +PR #168 does not implement Streamable HTTP transport parsing, complete request `_meta` validation, `tools/list` serialization/caching/pagination, OAuth, browser I/O, WebMCP/BiDi/CDP translation, secret delivery, persistence, or a complete OriginWeave Protocol adapter. Those remain separate adapter/runtime work. Protected `main` therefore must continue to describe MCP as planned until this active-PR evidence is integrated, and even after integration only the merged bounded routing foundation may be called implemented; the full adapter remains planned until its remaining acceptance boundaries ship. + +The version boundary is explicit: the routing foundation accepts only MCP `2026-07-28`; it does not infer compatibility with later protocol generations. OriginWeave Protocol versioning remains independent and cannot be changed by MCP metadata. + ## Consequences OriginWeave carries adapter maintenance and version negotiation but gains a durable customer API. Multiple browser/control transports can coexist. New upstream capabilities do not silently change risk or action semantics. Compatibility matrices become release artifacts. @@ -50,13 +58,15 @@ Protocol validation occurs before messages influence policy. Tool/page-provided Require version-negotiation tests, schema/property tests, malformed-message tests, BiDi/CDP semantic parity tests for shared capabilities, WebMCP prompt-injection tests, MCP authority-separation and version-change tests, browser-version compatibility matrices, and end-to-end proof that unsupported capabilities fail without side effects. +For active PR #168 specifically, acceptance additionally requires deterministic tool-name bounds/syntax, exact header/body method and tool-name correlation at the represented routing boundary, explicit unknown-tool rejection, one unambiguous tool-to-action registry, independent capability/risk expectations, route/action mismatch denial before ordinary policy evaluation, exact 100% owned-production coverage, and unchanged-head CI/security/review evidence. These checks do not substitute for complete transport or adapter conformance. + ## Migration and rollback Adapters are independently versioned and can be canaried. Clients migrate through OriginWeave Protocol compatibility rules, not upstream protocol rewrites. Rollback pins a previously supported adapter/browser/protocol pair and records that pair in provenance. ## Open follow-ups -Define internal protocol versioning rules, adapter capability descriptors, minimum supported BiDi level, CDP pin policy, and MCP/WebMCP schema isolation. +Define internal protocol versioning rules, adapter capability descriptors, minimum supported BiDi level, CDP pin policy, complete MCP Streamable HTTP/request-metadata validation, MCP discovery/serialization/cache behavior, and MCP/WebMCP schema isolation. ## Supersession / reversal conditions @@ -68,10 +78,12 @@ Chrome DevTools Protocol. (2026). *Chrome DevTools Protocol — latest (tip-of-t Chrome DevTools Protocol. (2026). *WebMCP domain*. Chromium. Retrieved August 9, 2026, from https://chromedevtools.github.io/devtools-protocol/tot/WebMCP/ +Model Context Protocol. (2026, July 28). *Specification: 2026-07-28*. https://modelcontextprotocol.io/specification/2026-07-28 + Parra, D. S., & Delimarsky, D. (2026, July 28). *The 2026-07-28 specification*. Model Context Protocol Blog. https://blog.modelcontextprotocol.io/posts/2026-07-28/ World Wide Web Consortium. (2026, June 29). *WebDriver BiDi* [Working Draft]. https://www.w3.org/TR/2026/WD-webdriver-bidi-20260629/ ## Related documents -See `docs/API_CONTRACT.md`, `docs/TRD.md`, `docs/doctoring/product-documentation-baseline.md`, and `docs/DATA_GOVERNANCE.md`. +See `docs/API_CONTRACT.md`, `docs/TRD.md`, `docs/doctoring.md`, `docs/doctoring/product-documentation-baseline.md`, `docs/traceability/README.md`, and `docs/DATA_GOVERNANCE.md`. From f9a1fa9c42c72c76b029f71c56acd8261cff338c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 18:39:03 +0900 Subject: [PATCH 19/26] docs(mcp): clarify active routing foundation --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 17085c05d..a956ff60b 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ OriginWeave is a Chromium-compatible, Rust-first control plane for governed AI agents on the web. It is designed to let an agent observe, extract, and act without turning untrusted page content into authority, exposing secrets to a model, connecting to an unapproved network destination, accepting an unauthenticated web service, or losing the evidence required to explain what happened. -> Project status: pre-alpha. The current repository contains independently reusable safety, resolved-destination, direct TCP peer-binding, and authenticated TLS service-identity kernels. Chromium, WebDriver BiDi, CDP, MCP, HTTP, proxy, WARC, and persistent provenance adapters are planned but not yet shipped. +> Project status: pre-alpha. The current protected repository contains independently reusable safety, resolved-destination, direct TCP peer-binding, and authenticated TLS service-identity kernels. Chromium, WebDriver BiDi, CDP, complete MCP, HTTP, proxy, WARC, and persistent provenance adapters are planned but not yet shipped. Active PR #168 implements only a bounded MCP `2026-07-28` stateless tool-routing and typed-action/policy foundation; it remains non-shipped active-PR evidence and does not make the complete MCP adapter available. ## Why OriginWeave @@ -40,6 +40,8 @@ The repository is organized as independently consumable Rust crates: - `originweave-resource`: task-level RAM, VRAM, thread, and frame-time budgets with cumulative mitigation plans. - `originweave-evidence`: universally value-redacted network evidence and source-bound provenance records. +Active PR #168 additionally carries a non-shipped `originweave-core` MCP routing registry and `originweave-policy` binding for the MCP `2026-07-28` `tools/call` boundary. That foundation validates and maps an explicit tool name to an existing typed action; it does not implement transport parsing, `tools/list`, OAuth, browser control, secret materialization, persistence, or ambient authority. + See [ARCHITECTURE.md](ARCHITECTURE.md) and the [architecture decision records](docs/adr/) for binding design decisions. ## Safety model @@ -97,7 +99,7 @@ isolated Chromium session → redacted provenance bundle ``` -Subsequent work connects the live Chromium network service, adds explicit proxy and download policy, WARC/PROV persistence, MCP and Browser Agent Protocol adapters, extension compatibility testing, GPU/RAM telemetry, prompt-injection benchmarks, and an accessible approval interface. See [docs/product-roadmap.md](docs/product-roadmap.md). +Subsequent work connects the live Chromium network service, adds explicit proxy and download policy, WARC/PROV persistence, completes the MCP and Browser Agent Protocol adapters beyond the active routing foundation, expands extension compatibility testing, adds GPU/RAM telemetry and prompt-injection benchmarks, and builds an accessible approval interface. See [docs/product-roadmap.md](docs/product-roadmap.md). ## Hourly product-development loop From abfb2612c3e9071709094513ab3c0f8849a740cb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 18:40:12 +0900 Subject: [PATCH 20/26] docs(mcp): add 2026-07-28 primary specification --- docs/doctoring.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/docs/doctoring.md b/docs/doctoring.md index f0133bb5d..7bf45f95f 100644 --- a/docs/doctoring.md +++ b/docs/doctoring.md @@ -8,6 +8,8 @@ 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. +The final Model Context Protocol `2026-07-28` specification defines the currently reviewed MCP generation. Its stateless request model carries protocol metadata per request and standard Streamable HTTP routing metadata for MCP operations; its Tools surface defines bounded, case-sensitive tool names and requires clients to treat tool annotations as untrusted unless supplied by a trusted server. OriginWeave therefore keeps MCP outside the product authority model. Active PR #168 implements only a bounded Rust `tools/call` routing/action-policy foundation for that exact generation; the complete transport, request-metadata, discovery, OAuth, browser, secret, and persistence adapter remains planned and cannot be inferred from the core routing primitive. + ### 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`. @@ -142,6 +144,8 @@ Lodderstedt, T., Bradley, J., Labunets, A., & Fett, D. (2025). *OAuth 2.0 securi Microsoft. (2025, July 25). *Azure IP address 168.63.129.16 overview*. Microsoft Learn. https://learn.microsoft.com/azure/virtual-network/what-is-ip-address-168-63-129-16 +Model Context Protocol. (2026, July 28). *Specification: 2026-07-28*. https://modelcontextprotocol.io/specification/2026-07-28 + Nielsen, S., Cetin, E., Schwendeman, P., Sun, Q., Xu, J., & Tang, Y. (2025). *Learning to orchestrate agents in natural language with the Conductor* [Preprint]. arXiv. https://doi.org/10.48550/arXiv.2512.04388 Rescorla, E. (2026). *The Transport Layer Security (TLS) protocol version 1.3* (RFC 9846). Internet Engineering Task Force. https://doi.org/10.17487/RFC9846 From 7d1b610cb215c8f1b32727654972bd0ed17c5280 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Mon, 17 Aug 2026 18:40:56 +0900 Subject: [PATCH 21/26] docs(mcp): trace active routing boundary --- docs/traceability/mcp-authority-route.md | 53 ++++++++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 docs/traceability/mcp-authority-route.md diff --git a/docs/traceability/mcp-authority-route.md b/docs/traceability/mcp-authority-route.md new file mode 100644 index 000000000..bea49533a --- /dev/null +++ b/docs/traceability/mcp-authority-route.md @@ -0,0 +1,53 @@ +# MCP 2026-07-28 authority-route traceability + +- **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` +- **Owning work:** PR #168 `feat/mcp): bind stateless tool routing to typed actions` +- **Protected-main status:** non-shipped active-PR evidence +- **Complete MCP adapter status:** `PLANNED` +- **Governing decision:** ADR 0107 + +## Scope + +PR #168 implements a bounded Rust control-plane foundation for MCP `2026-07-28` `tools/call` routing. It validates the represented stateless routing envelope, bounds and syntax-validates both attacker-controlled tool-name fields before correlation, maps only an explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from the same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. + +A successful `ValidatedMcpToolCall` proves routing integrity only. It grants no capability, origin, approval, secret, browser, tenant, persistence, network, or evidence authority. `originweave_policy::evaluate_mcp` still delegates to the ordinary policy evaluator after the route/action match. + +## Product-status reconciliation + +`docs/PRD.md` PRD-INT-004 and `docs/TRD.md` Section 12 intentionally remain **Planned** at the complete-adapter level. That status is not contradicted by this active PR: the PR implements only a reusable routing/action-policy foundation below the product adapter. `README.md` and `CHANGELOG.md` therefore distinguish the active foundation from shipped protected-main capability, and ADR 0107 records the same version and authority boundary. + +The following remain outside PR #168 and must not be inferred from it: + +- Streamable HTTP transport parsing and header materialization; +- complete request `_meta` validation, including per-request client capabilities; +- `tools/list` serialization, pagination, cache semantics, and subscription handling; +- OAuth and authenticated MCP deployment policy; +- browser-control I/O or BiDi/CDP/WebMCP translation; +- secret materialization or broker transport; +- persistence, durable audit storage, or WARC/PROV export; and +- an OriginWeave Protocol version transition. + +## Version boundary + +The active routing foundation accepts only protocol generation `2026-07-28`. MCP versioning is independent of the OriginWeave Protocol. A later MCP revision does not silently change OriginWeave action, risk, capability, approval, secret, origin, tenant, browser, or evidence semantics. + +The reviewed primary source is: + +Model Context Protocol. (2026, July 28). *Specification: 2026-07-28*. https://modelcontextprotocol.io/specification/2026-07-28 + +The canonical bibliography remains `docs/doctoring.md`. + +## Executable evidence + +Current PR #168 production/test surfaces include: + +- `crates/originweave-core/src/mcp.rs` — bounded deterministic catalog and `ValidatedMcpToolCall` routing primitive; +- `crates/originweave-core/tests/mcp_authority_route.rs` — mapping, bounds, malformed-input, version/method/header-body, and error-contract evidence; +- `crates/originweave-policy/src/lib.rs` — `evaluate_mcp` route/action guard before normal policy evaluation; and +- `crates/originweave-policy/tests/mcp_route_binding.rs` — confused-deputy and policy-preservation evidence. + +Exact current-head CI/security/review evidence must be regenerated after every branch mutation. Predecessor-head success is historical only. + +## Promotion rule + +This dossier may change to `IMPLEMENTED_ON_PROTECTED_MAIN` for the bounded routing foundation only after PR #168 reaches protected `main` under live governance and exact-head acceptance. That promotion still does **not** promote the complete MCP adapter from `PLANNED`; each remaining transport/runtime boundary requires its own integrated evidence. From 908a15b363fddf7e53b0195c4966286f01d5b49c Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:42:12 -0700 Subject: [PATCH 22/26] test(mcp): bound untrusted method metadata --- .../tests/mcp_authority_route.rs | 56 ++++++++++++++++++- 1 file changed, 54 insertions(+), 2 deletions(-) diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs index a1b1b6fc3..4722776e6 100644 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -1,8 +1,8 @@ use std::error::Error; use originweave_core::mcp::{ - MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, - ValidatedMcpToolCall, supported_mcp_tools, + MAX_MCP_METHOD_NAME_BYTES, MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, ValidatedMcpToolCall, supported_mcp_tools, }; use originweave_core::{ActionKind, Capability, RiskClass}; @@ -193,6 +193,54 @@ fn mcp_route_rejects_protocol_header_body_and_method_drift() { ); } +#[test] +fn mcp_route_bounds_each_untrusted_method_before_cross_field_comparison() { + let at_limit = "x".repeat(MAX_MCP_METHOD_NAME_BYTES); + let oversized_routing = "r".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); + let oversized_body = "b".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); + + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + &oversized_routing, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + &oversized_body, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + "tools call", + "originweave.observe", + "tools call", + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + &at_limit, + "originweave.observe", + &at_limit, + "originweave.observe", + ), + Err(McpToolBoundaryError::UnsupportedMethod) + ); +} + #[test] fn mcp_route_rejects_unbounded_malformed_and_unmapped_tool_names() { let at_limit = "x".repeat(MAX_MCP_TOOL_NAME_BYTES); @@ -273,6 +321,10 @@ fn mcp_boundary_errors_are_deterministic_and_do_not_echo_untrusted_values() { McpToolBoundaryError::UnsupportedMethod, "only MCP tools/call requests can enter the typed action boundary", ), + ( + McpToolBoundaryError::InvalidMethod, + "MCP method violates the bounded ASCII routing syntax", + ), ( McpToolBoundaryError::InvalidToolName, "MCP tool name violates the bounded ASCII routing syntax", From b1a5a32f5f8077b99628e561f1ad73d6911fe3f4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:43:45 -0700 Subject: [PATCH 23/26] fix(mcp): bound untrusted method metadata --- crates/originweave-core/src/mcp.rs | 26 +++++++++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) diff --git a/crates/originweave-core/src/mcp.rs b/crates/originweave-core/src/mcp.rs index dd8d60258..b026d5e61 100644 --- a/crates/originweave-core/src/mcp.rs +++ b/crates/originweave-core/src/mcp.rs @@ -17,6 +17,9 @@ pub const MCP_PROTOCOL_VERSION: &str = "2026-07-28"; /// The only MCP method that can enter the typed action-routing boundary. pub const MCP_TOOLS_CALL_METHOD: &str = "tools/call"; +/// Maximum accepted MCP method-name length in bytes. +pub const MAX_MCP_METHOD_NAME_BYTES: usize = 64; + /// Maximum accepted MCP tool-name length in bytes. pub const MAX_MCP_TOOL_NAME_BYTES: usize = 128; @@ -123,6 +126,8 @@ pub enum McpToolBoundaryError { UnsupportedProtocolVersion, /// MCP routing metadata disagrees with the method or tool name in the body. HeaderBodyMismatch, + /// The request method violates the bounded ASCII MCP routing syntax. + InvalidMethod, /// The request method is not the supported `tools/call` operation. UnsupportedMethod, /// The tool name violates the bounded ASCII MCP routing syntax. @@ -140,6 +145,9 @@ impl fmt::Display for McpToolBoundaryError { Self::HeaderBodyMismatch => { formatter.write_str("MCP routing headers do not match the request body") } + Self::InvalidMethod => { + formatter.write_str("MCP method violates the bounded ASCII routing syntax") + } Self::UnsupportedMethod => formatter .write_str("only MCP tools/call requests can enter the typed action boundary"), Self::InvalidToolName => { @@ -167,9 +175,9 @@ impl ValidatedMcpToolCall { /// Routing integrity is intentionally narrower than authorization. A /// successful value proves only that the untrusted protocol version, /// routing metadata, body method, and body tool name agree with one - /// explicitly supported mapping. Each untrusted tool name is shape-validated - /// before cross-field comparison so malformed or oversized names cannot - /// bypass the bounded routing syntax through mismatch handling. + /// explicitly supported mapping. Each untrusted method and tool name is + /// shape-validated before cross-field comparison so malformed or oversized + /// metadata cannot bypass the bounded routing syntax through mismatch handling. pub fn new( protocol_version: &str, routing_method: &str, @@ -180,6 +188,9 @@ impl ValidatedMcpToolCall { if protocol_version != MCP_PROTOCOL_VERSION { return Err(McpToolBoundaryError::UnsupportedProtocolVersion); } + if !valid_method(routing_method) || !valid_method(body_method) { + return Err(McpToolBoundaryError::InvalidMethod); + } if !valid_tool_name(routing_tool_name) || !valid_tool_name(body_tool_name) { return Err(McpToolBoundaryError::InvalidToolName); } @@ -210,6 +221,15 @@ impl ValidatedMcpToolCall { } } +fn valid_method(method: &str) -> bool { + if method.is_empty() || method.len() > MAX_MCP_METHOD_NAME_BYTES { + return false; + } + method + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-' | b'.' | b'/')) +} + fn valid_tool_name(tool_name: &str) -> bool { if tool_name.is_empty() || tool_name.len() > MAX_MCP_TOOL_NAME_BYTES { return false; From 77b30b1d065e09a6ea202dc9fdc9c6e765137d37 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:48:21 -0700 Subject: [PATCH 24/26] test(mcp): cover empty method boundary --- .../tests/mcp_authority_route.rs | 20 +++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/crates/originweave-core/tests/mcp_authority_route.rs b/crates/originweave-core/tests/mcp_authority_route.rs index 4722776e6..80357ec63 100644 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ b/crates/originweave-core/tests/mcp_authority_route.rs @@ -199,6 +199,26 @@ fn mcp_route_bounds_each_untrusted_method_before_cross_field_comparison() { let oversized_routing = "r".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); let oversized_body = "b".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + "", + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + "", + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); assert_eq!( ValidatedMcpToolCall::new( MCP_PROTOCOL_VERSION, From eb0b3e5af93eb0f1f9931ebd74f2c76cc08511c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:52:09 -0700 Subject: [PATCH 25/26] docs(mcp): record bounded method routing contract --- docs/adr/0107-browser-protocol-adapter-strategy.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/adr/0107-browser-protocol-adapter-strategy.md b/docs/adr/0107-browser-protocol-adapter-strategy.md index 1c71105d2..e3c0bf657 100644 --- a/docs/adr/0107-browser-protocol-adapter-strategy.md +++ b/docs/adr/0107-browser-protocol-adapter-strategy.md @@ -36,7 +36,7 @@ MCP version negotiation is independent of the OriginWeave Protocol version. As o ### Current implementation boundary -The complete MCP adapter remains **Planned**. Active PR #168 is narrower **IMPLEMENTED_ON_ACTIVE_PR** evidence inside the Rust control plane: it validates the `2026-07-28` stateless `tools/call` routing envelope presented to this boundary, bounds and syntax-checks both untrusted tool-name fields, derives one of the existing typed `ActionKind` values from a deterministic reviewed registry, exposes discovery metadata from that same registry, and requires the resulting action to pass the ordinary OriginWeave policy evaluator. The catalog and validated route grant no capability, approval, origin, secret, browser, persistence, or evidence authority by themselves. +The complete MCP adapter remains **Planned**. Active PR #168 is narrower **IMPLEMENTED_ON_ACTIVE_PR** evidence inside the Rust control plane: it validates the `2026-07-28` stateless `tools/call` routing envelope presented to this boundary, bounds and syntax-checks both untrusted method fields and both untrusted tool-name fields before cross-field correlation, derives one of the existing typed `ActionKind` values from a deterministic reviewed registry, exposes discovery metadata from that same registry, and requires the resulting action to pass the ordinary OriginWeave policy evaluator. The method boundary accepts only nonempty ASCII method names up to 64 bytes using the reviewed routing alphabet, while the tool-name boundary accepts only nonempty ASCII names up to 128 bytes using its narrower reviewed alphabet. The catalog and validated route grant no capability, approval, origin, secret, browser, persistence, or evidence authority by themselves. PR #168 does not implement Streamable HTTP transport parsing, complete request `_meta` validation, `tools/list` serialization/caching/pagination, OAuth, browser I/O, WebMCP/BiDi/CDP translation, secret delivery, persistence, or a complete OriginWeave Protocol adapter. Those remain separate adapter/runtime work. Protected `main` therefore must continue to describe MCP as planned until this active-PR evidence is integrated, and even after integration only the merged bounded routing foundation may be called implemented; the full adapter remains planned until its remaining acceptance boundaries ship. @@ -52,13 +52,13 @@ Adapter negotiation failure disables only affected capabilities. Unsupported or ## Security / privacy / governance impact -Protocol validation occurs before messages influence policy. Tool/page-provided strings remain untrusted. Secret handles never become raw secret protocol payloads; only the separately authorized trusted broker-to-browser delivery path may materialize the value, and that value does not pass through MCP, WebMCP, BiDi observation, or model-visible CDP output. Adapter version/provenance is recorded for audit and incident reconstruction. +Protocol validation occurs before messages influence policy. Tool/page-provided strings remain untrusted. Method and tool routing metadata is shape-bounded before correlation, preventing malformed or oversized untrusted routing strings from being reinterpreted through mismatch handling. Secret handles never become raw secret protocol payloads; only the separately authorized trusted broker-to-browser delivery path may materialize the value, and that value does not pass through MCP, WebMCP, BiDi observation, or model-visible CDP output. Adapter version/provenance is recorded for audit and incident reconstruction. ## Tests and acceptance evidence Require version-negotiation tests, schema/property tests, malformed-message tests, BiDi/CDP semantic parity tests for shared capabilities, WebMCP prompt-injection tests, MCP authority-separation and version-change tests, browser-version compatibility matrices, and end-to-end proof that unsupported capabilities fail without side effects. -For active PR #168 specifically, acceptance additionally requires deterministic tool-name bounds/syntax, exact header/body method and tool-name correlation at the represented routing boundary, explicit unknown-tool rejection, one unambiguous tool-to-action registry, independent capability/risk expectations, route/action mismatch denial before ordinary policy evaluation, exact 100% owned-production coverage, and unchanged-head CI/security/review evidence. These checks do not substitute for complete transport or adapter conformance. +For active PR #168 specifically, acceptance additionally requires deterministic method and tool-name bounds/syntax, exact header/body method and tool-name correlation only after both sides are bounded, explicit invalid-method/invalid-tool-name/unknown-tool rejection, one unambiguous tool-to-action registry, independent capability/risk expectations, route/action mismatch denial before ordinary policy evaluation, exact 100% owned-production coverage, and unchanged-head CI/security/review evidence. These checks do not substitute for complete transport or adapter conformance. ## Migration and rollback From ecc01e821c99f4b002153e725f20ddd17c58df3f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 18 Aug 2026 05:52:34 -0700 Subject: [PATCH 26/26] docs(mcp): trace bounded method metadata --- docs/traceability/mcp-authority-route.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/traceability/mcp-authority-route.md b/docs/traceability/mcp-authority-route.md index bea49533a..ddbd5927c 100644 --- a/docs/traceability/mcp-authority-route.md +++ b/docs/traceability/mcp-authority-route.md @@ -1,14 +1,14 @@ # MCP 2026-07-28 authority-route traceability - **Capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -- **Owning work:** PR #168 `feat/mcp): bind stateless tool routing to typed actions` +- **Owning work:** PR #168 `feat(mcp): bind stateless tool routing to typed actions` - **Protected-main status:** non-shipped active-PR evidence - **Complete MCP adapter status:** `PLANNED` - **Governing decision:** ADR 0107 ## Scope -PR #168 implements a bounded Rust control-plane foundation for MCP `2026-07-28` `tools/call` routing. It validates the represented stateless routing envelope, bounds and syntax-validates both attacker-controlled tool-name fields before correlation, maps only an explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from the same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. +PR #168 implements a bounded Rust control-plane foundation for MCP `2026-07-28` `tools/call` routing. It validates the represented stateless routing envelope, bounds and syntax-validates both attacker-controlled method fields and both attacker-controlled tool-name fields before correlation, maps only an explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from the same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. Methods are nonempty reviewed-ASCII routing tokens of at most 64 bytes; tool names are nonempty reviewed-ASCII identifiers of at most 128 bytes. Invalid method metadata is rejected distinctly from a bounded but unsupported MCP method. A successful `ValidatedMcpToolCall` proves routing integrity only. It grants no capability, origin, approval, secret, browser, tenant, persistence, network, or evidence authority. `originweave_policy::evaluate_mcp` still delegates to the ordinary policy evaluator after the route/action match. @@ -41,8 +41,8 @@ The canonical bibliography remains `docs/doctoring.md`. Current PR #168 production/test surfaces include: -- `crates/originweave-core/src/mcp.rs` — bounded deterministic catalog and `ValidatedMcpToolCall` routing primitive; -- `crates/originweave-core/tests/mcp_authority_route.rs` — mapping, bounds, malformed-input, version/method/header-body, and error-contract evidence; +- `crates/originweave-core/src/mcp.rs` — bounded deterministic catalog plus method/tool routing validation in the `ValidatedMcpToolCall` primitive; +- `crates/originweave-core/tests/mcp_authority_route.rs` — mapping, exact method/tool bounds, empty/oversized/malformed inputs, version/method/header-body correlation, and error-contract evidence; - `crates/originweave-policy/src/lib.rs` — `evaluate_mcp` route/action guard before normal policy evaluation; and - `crates/originweave-policy/tests/mcp_route_binding.rs` — confused-deputy and policy-preservation evidence.