From ca95278998b05668bd0037e928f22d6a7a931ce3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:06:17 +0900 Subject: [PATCH 01/50] test(ddd): require MCP adapter bounded context --- tests/test_ddd_boundaries.py | 43 ++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/test_ddd_boundaries.py diff --git a/tests/test_ddd_boundaries.py b/tests/test_ddd_boundaries.py new file mode 100644 index 000000000..8da716cf1 --- /dev/null +++ b/tests/test_ddd_boundaries.py @@ -0,0 +1,43 @@ +"""Architectural fitness tests for bounded-context ownership.""" + +from __future__ import annotations + +import pathlib +import tomllib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +class DomainBoundaryTests(unittest.TestCase): + """Keep protocol adapters out of the shared domain-contract kernel.""" + + def test_mcp_protocol_contract_has_its_own_adapter_crate(self) -> None: + """MCP routing DTOs belong to the MCP adapter, not originweave-core.""" + + workspace = tomllib.loads((ROOT / "Cargo.toml").read_text(encoding="utf-8")) + self.assertIn("crates/originweave-mcp", workspace["workspace"]["members"]) + self.assertFalse((ROOT / "crates/originweave-core/src/mcp.rs").exists()) + + mcp_manifest = tomllib.loads( + (ROOT / "crates/originweave-mcp/Cargo.toml").read_text(encoding="utf-8") + ) + self.assertEqual( + set(mcp_manifest.get("dependencies", {})), + {"originweave-core"}, + ) + + policy_manifest = tomllib.loads( + (ROOT / "crates/originweave-policy/Cargo.toml").read_text(encoding="utf-8") + ) + self.assertIn("originweave-mcp", policy_manifest["dependencies"]) + + policy_source = (ROOT / "crates/originweave-policy/src/lib.rs").read_text( + encoding="utf-8" + ) + self.assertNotIn("originweave_core::mcp", policy_source) + self.assertIn("originweave_mcp::ValidatedMcpToolCall", policy_source) + + +if __name__ == "__main__": + unittest.main() From 9a2ad26f81ef57729a226a265ea0400e68f3f1c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:10:49 +0900 Subject: [PATCH 02/50] refactor(ddd): register MCP adapter crate --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index 0d5ab469c..eb6d6613b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,6 +2,7 @@ members = [ "crates/originweave-core", "crates/originweave-bap", + "crates/originweave-mcp", "crates/originweave-policy", "crates/originweave-resource", "crates/originweave-evidence", From ba8cb4ffbb134999cf0b5327e79cf2b33d4f2176 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:11:03 +0900 Subject: [PATCH 03/50] refactor(ddd): add MCP adapter manifest --- crates/originweave-mcp/Cargo.toml | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 crates/originweave-mcp/Cargo.toml diff --git a/crates/originweave-mcp/Cargo.toml b/crates/originweave-mcp/Cargo.toml new file mode 100644 index 000000000..a86809bda --- /dev/null +++ b/crates/originweave-mcp/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "originweave-mcp" +description = "OriginWeave MCP adapter 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 + +[dependencies] +originweave-core = { path = "../originweave-core" } + +[lints] +workspace = true From 89b5389d49d3089827024f591945b2614c72c337 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:11:10 +0900 Subject: [PATCH 04/50] refactor(ddd): establish MCP adapter root --- crates/originweave-mcp/src/lib.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 crates/originweave-mcp/src/lib.rs diff --git a/crates/originweave-mcp/src/lib.rs b/crates/originweave-mcp/src/lib.rs new file mode 100644 index 000000000..239430598 --- /dev/null +++ b/crates/originweave-mcp/src/lib.rs @@ -0,0 +1,15 @@ +//! Fail-closed MCP adapter contracts for OriginWeave. +//! +//! This crate owns MCP protocol-generation, discovery, and stateless tool-routing +//! contracts. It maps reviewed MCP protocol values into existing OriginWeave +//! action contracts but grants no policy, browser, network, secret, or evidence +//! authority. + +#![forbid(unsafe_code)] +#![deny(missing_docs)] + +pub(crate) use originweave_core::{ActionKind, Capability, RiskClass}; + +mod routing; + +pub use routing::*; From b7d67a1af7d67962943204e31e9cb515c69b763f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:11:31 +0900 Subject: [PATCH 05/50] refactor(ddd): move MCP routing out of core --- .../src/mcp.rs => originweave-mcp/src/routing.rs} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename crates/{originweave-core/src/mcp.rs => originweave-mcp/src/routing.rs} (100%) diff --git a/crates/originweave-core/src/mcp.rs b/crates/originweave-mcp/src/routing.rs similarity index 100% rename from crates/originweave-core/src/mcp.rs rename to crates/originweave-mcp/src/routing.rs From 4dceac184435651b0c69d0332aa9326a43663c7e Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:11:47 +0900 Subject: [PATCH 06/50] refactor(ddd): remove MCP adapter from core --- crates/originweave-core/src/root.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/originweave-core/src/root.rs b/crates/originweave-core/src/root.rs index c47a136d4..e39f9a598 100644 --- a/crates/originweave-core/src/root.rs +++ b/crates/originweave-core/src/root.rs @@ -11,7 +11,5 @@ mod contracts; pub use contracts::*; -/// Stateless MCP routing validation that maps only explicit tools to typed actions. -pub mod mcp; /// Deterministic fail-closed release benchmark acceptance aggregation. pub mod release_acceptance; From 20f84582828347f55fcea110215e8efb7ea7c8e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:12:01 +0900 Subject: [PATCH 07/50] refactor(ddd): depend on MCP adapter boundary --- crates/originweave-policy/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-policy/Cargo.toml b/crates/originweave-policy/Cargo.toml index 18732be0b..c56f59222 100644 --- a/crates/originweave-policy/Cargo.toml +++ b/crates/originweave-policy/Cargo.toml @@ -12,6 +12,7 @@ publish = false [dependencies] originweave-core = { path = "../originweave-core" } +originweave-mcp = { path = "../originweave-mcp" } [lints] workspace = true From bcbed2cd041e0394df6781b2aece1c596b3b9a90 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:12:28 +0900 Subject: [PATCH 08/50] refactor(ddd): bind policy to MCP adapter contract --- crates/originweave-policy/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index dbfb3c16d..4c085bd68 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -15,11 +15,11 @@ 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, }; +use originweave_mcp::ValidatedMcpToolCall; /// The result of evaluating one typed action request. #[derive(Debug, Clone, PartialEq, Eq)] From 8be7521c40ecd8731bbdfe008532d8b3040265ac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:13:19 +0900 Subject: [PATCH 09/50] refactor(ddd): move MCP routing tests --- .../tests/mcp_authority_route.rs | 362 ++++++++++++++++++ 1 file changed, 362 insertions(+) create mode 100644 crates/originweave-mcp/tests/mcp_authority_route.rs diff --git a/crates/originweave-mcp/tests/mcp_authority_route.rs b/crates/originweave-mcp/tests/mcp_authority_route.rs new file mode 100644 index 000000000..e469eb2df --- /dev/null +++ b/crates/originweave-mcp/tests/mcp_authority_route.rs @@ -0,0 +1,362 @@ +use std::error::Error; + +use originweave_core::{ActionKind, Capability, RiskClass}; +use originweave_mcp::{ + MAX_MCP_METHOD_NAME_BYTES, MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, ValidatedMcpToolCall, supported_mcp_tools, +}; + +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, + 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, 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_capability + ); + assert_eq!(call.action_kind().risk_class(), expected_risk); + } + Ok(()) +} + +#[test] +fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> 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!( + 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_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, + "", + "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, + &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); + 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(&at_limit), Err(McpToolBoundaryError::UnknownTool)); + assert_eq!( + validate("originweave.legal_consent"), + Err(McpToolBoundaryError::UnknownTool) + ); + assert_eq!( + validate("third_party.arbitrary_javascript"), + Err(McpToolBoundaryError::UnknownTool) + ); +} + +#[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 = [ + ( + 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::InvalidMethod, + "MCP method violates the bounded ASCII routing syntax", + ), + ( + 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 03528ff0c942c0f311b668a8ed7ebc05cc412cc0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:13:45 +0900 Subject: [PATCH 10/50] refactor(ddd): move MCP discovery tests --- .../tests/mcp_tools_list_cache.rs | 221 ++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 crates/originweave-mcp/tests/mcp_tools_list_cache.rs diff --git a/crates/originweave-mcp/tests/mcp_tools_list_cache.rs b/crates/originweave-mcp/tests/mcp_tools_list_cache.rs new file mode 100644 index 000000000..0d9b903e2 --- /dev/null +++ b/crates/originweave-mcp/tests/mcp_tools_list_cache.rs @@ -0,0 +1,221 @@ +use std::error::Error; + +use originweave_mcp::{ + MAX_MCP_METHOD_NAME_BYTES, MCP_PROTOCOL_VERSION, MCP_TOOLS_LIST_METHOD, McpCacheScope, + McpResultType, McpToolsListBoundaryError, ValidatedMcpToolsListRequest, mcp_tools_list_page, + supported_mcp_tools, +}; + +#[test] +fn mcp_tools_list_page_is_complete_private_and_immediately_stale() { + let page = mcp_tools_list_page(); + + assert_eq!(page.result_type(), McpResultType::Complete); + assert_eq!(page.tools(), supported_mcp_tools()); + assert_eq!(page.ttl_ms(), 0); + assert_eq!(page.cache_scope(), McpCacheScope::Private); + assert_eq!(page.next_cursor(), None); +} + +fn valid_tools_list_request( + cursor: Option<&str>, +) -> Result { + ValidatedMcpToolsListRequest::new( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, + MCP_TOOLS_LIST_METHOD, + MCP_TOOLS_LIST_METHOD, + cursor, + ) +} + +#[test] +fn mcp_tools_list_request_requires_complete_request_metadata() { + assert_eq!( + valid_tools_list_request(None).map(|validated| validated.method()), + Ok(MCP_TOOLS_LIST_METHOD) + ); + + assert_eq!( + ValidatedMcpToolsListRequest::new( + None, + Some(MCP_PROTOCOL_VERSION), + true, + MCP_TOOLS_LIST_METHOD, + MCP_TOOLS_LIST_METHOD, + None, + ), + Err(McpToolsListBoundaryError::MissingProtocolVersionHeader) + ); + assert_eq!( + ValidatedMcpToolsListRequest::new( + Some(MCP_PROTOCOL_VERSION), + None, + true, + MCP_TOOLS_LIST_METHOD, + MCP_TOOLS_LIST_METHOD, + None, + ), + Err(McpToolsListBoundaryError::MissingProtocolVersionMetadata) + ); + assert_eq!( + ValidatedMcpToolsListRequest::new( + Some(MCP_PROTOCOL_VERSION), + Some("2025-11-25"), + true, + MCP_TOOLS_LIST_METHOD, + MCP_TOOLS_LIST_METHOD, + None, + ), + Err(McpToolsListBoundaryError::ProtocolVersionHeaderBodyMismatch) + ); + assert_eq!( + ValidatedMcpToolsListRequest::new( + Some("2025-11-25"), + Some("2025-11-25"), + true, + MCP_TOOLS_LIST_METHOD, + MCP_TOOLS_LIST_METHOD, + None, + ), + Err(McpToolsListBoundaryError::UnsupportedProtocolVersion) + ); + assert_eq!( + ValidatedMcpToolsListRequest::new( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + false, + MCP_TOOLS_LIST_METHOD, + MCP_TOOLS_LIST_METHOD, + None, + ), + Err(McpToolsListBoundaryError::MissingClientCapabilities) + ); +} + +#[test] +fn mcp_tools_list_bounds_protocol_metadata_before_cross_field_comparison() { + let oversized_protocol_version = format!("{MCP_PROTOCOL_VERSION}0"); + + for (header, metadata) in [ + (oversized_protocol_version.as_str(), MCP_PROTOCOL_VERSION), + (MCP_PROTOCOL_VERSION, oversized_protocol_version.as_str()), + ] { + assert_eq!( + ValidatedMcpToolsListRequest::new( + Some(header), + Some(metadata), + true, + MCP_TOOLS_LIST_METHOD, + MCP_TOOLS_LIST_METHOD, + None, + ), + Err(McpToolsListBoundaryError::UnsupportedProtocolVersion) + ); + } +} + +#[test] +fn mcp_tools_list_validates_each_method_before_cross_field_comparison() { + let oversized_method = "a".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); + + for (routing_method, body_method) in [ + ("tools list", MCP_TOOLS_LIST_METHOD), + (MCP_TOOLS_LIST_METHOD, "tools list"), + (oversized_method.as_str(), MCP_TOOLS_LIST_METHOD), + (MCP_TOOLS_LIST_METHOD, oversized_method.as_str()), + ] { + assert_eq!( + ValidatedMcpToolsListRequest::new( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, + routing_method, + body_method, + None, + ), + Err(McpToolsListBoundaryError::InvalidMethod) + ); + } +} + +#[test] +fn mcp_tools_list_request_requires_exact_routing_and_no_unissued_cursor() { + assert_eq!( + ValidatedMcpToolsListRequest::new( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, + MCP_TOOLS_LIST_METHOD, + "tools/call", + None, + ), + Err(McpToolsListBoundaryError::MethodHeaderBodyMismatch) + ); + assert_eq!( + ValidatedMcpToolsListRequest::new( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, + "resources/list", + "resources/list", + None, + ), + Err(McpToolsListBoundaryError::UnsupportedMethod) + ); + + for cursor in ["cursor-1", ""] { + assert_eq!( + valid_tools_list_request(Some(cursor)), + Err(McpToolsListBoundaryError::UnsupportedCursor) + ); + } +} + +#[test] +fn mcp_tools_list_request_errors_are_source_free_and_non_echoing() { + let cases = [ + ( + McpToolsListBoundaryError::MissingProtocolVersionHeader, + "MCP protocol version header is required", + ), + ( + McpToolsListBoundaryError::MissingProtocolVersionMetadata, + "MCP request metadata protocol version is required", + ), + ( + McpToolsListBoundaryError::ProtocolVersionHeaderBodyMismatch, + "MCP protocol version header does not match request metadata", + ), + ( + McpToolsListBoundaryError::UnsupportedProtocolVersion, + "unsupported MCP protocol version", + ), + ( + McpToolsListBoundaryError::MissingClientCapabilities, + "MCP request metadata client capabilities are required", + ), + ( + McpToolsListBoundaryError::InvalidMethod, + "MCP method violates the bounded ASCII routing syntax", + ), + ( + McpToolsListBoundaryError::MethodHeaderBodyMismatch, + "MCP method header does not match the request body", + ), + ( + McpToolsListBoundaryError::UnsupportedMethod, + "only MCP tools/list requests can enter the discovery boundary", + ), + ( + McpToolsListBoundaryError::UnsupportedCursor, + "MCP tools/list cursor was not issued by this fixed catalog", + ), + ]; + + for (error, expected) in cases { + assert_eq!(error.to_string(), expected); + assert!(error.source().is_none()); + } +} From 2413f1739e427ef656614f36e6a0cb356eea44d8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:13:55 +0900 Subject: [PATCH 11/50] refactor(ddd): remove MCP test from core --- .../tests/mcp_authority_route.rs | 362 ------------------ 1 file changed, 362 deletions(-) delete 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 deleted file mode 100644 index 80357ec63..000000000 --- a/crates/originweave-core/tests/mcp_authority_route.rs +++ /dev/null @@ -1,362 +0,0 @@ -use std::error::Error; - -use originweave_core::mcp::{ - 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}; - -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, - 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, 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_capability - ); - assert_eq!(call.action_kind().risk_class(), expected_risk); - } - Ok(()) -} - -#[test] -fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> 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!( - 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_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, - "", - "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, - &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); - 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(&at_limit), Err(McpToolBoundaryError::UnknownTool)); - assert_eq!( - validate("originweave.legal_consent"), - Err(McpToolBoundaryError::UnknownTool) - ); - assert_eq!( - validate("third_party.arbitrary_javascript"), - Err(McpToolBoundaryError::UnknownTool) - ); -} - -#[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 = [ - ( - 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::InvalidMethod, - "MCP method violates the bounded ASCII routing syntax", - ), - ( - 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 6d54e024ae1c3f1422e54b4934e4e9f8ade79137 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:14:04 +0900 Subject: [PATCH 12/50] refactor(ddd): remove MCP discovery test from core --- .../tests/mcp_tools_list_cache.rs | 221 ------------------ 1 file changed, 221 deletions(-) delete mode 100644 crates/originweave-core/tests/mcp_tools_list_cache.rs diff --git a/crates/originweave-core/tests/mcp_tools_list_cache.rs b/crates/originweave-core/tests/mcp_tools_list_cache.rs deleted file mode 100644 index 9d3681673..000000000 --- a/crates/originweave-core/tests/mcp_tools_list_cache.rs +++ /dev/null @@ -1,221 +0,0 @@ -use std::error::Error; - -use originweave_core::mcp::{ - MAX_MCP_METHOD_NAME_BYTES, MCP_PROTOCOL_VERSION, MCP_TOOLS_LIST_METHOD, McpCacheScope, - McpResultType, McpToolsListBoundaryError, ValidatedMcpToolsListRequest, mcp_tools_list_page, - supported_mcp_tools, -}; - -#[test] -fn mcp_tools_list_page_is_complete_private_and_immediately_stale() { - let page = mcp_tools_list_page(); - - assert_eq!(page.result_type(), McpResultType::Complete); - assert_eq!(page.tools(), supported_mcp_tools()); - assert_eq!(page.ttl_ms(), 0); - assert_eq!(page.cache_scope(), McpCacheScope::Private); - assert_eq!(page.next_cursor(), None); -} - -fn valid_tools_list_request( - cursor: Option<&str>, -) -> Result { - ValidatedMcpToolsListRequest::new( - Some(MCP_PROTOCOL_VERSION), - Some(MCP_PROTOCOL_VERSION), - true, - MCP_TOOLS_LIST_METHOD, - MCP_TOOLS_LIST_METHOD, - cursor, - ) -} - -#[test] -fn mcp_tools_list_request_requires_complete_request_metadata() { - assert_eq!( - valid_tools_list_request(None).map(|validated| validated.method()), - Ok(MCP_TOOLS_LIST_METHOD) - ); - - assert_eq!( - ValidatedMcpToolsListRequest::new( - None, - Some(MCP_PROTOCOL_VERSION), - true, - MCP_TOOLS_LIST_METHOD, - MCP_TOOLS_LIST_METHOD, - None, - ), - Err(McpToolsListBoundaryError::MissingProtocolVersionHeader) - ); - assert_eq!( - ValidatedMcpToolsListRequest::new( - Some(MCP_PROTOCOL_VERSION), - None, - true, - MCP_TOOLS_LIST_METHOD, - MCP_TOOLS_LIST_METHOD, - None, - ), - Err(McpToolsListBoundaryError::MissingProtocolVersionMetadata) - ); - assert_eq!( - ValidatedMcpToolsListRequest::new( - Some(MCP_PROTOCOL_VERSION), - Some("2025-11-25"), - true, - MCP_TOOLS_LIST_METHOD, - MCP_TOOLS_LIST_METHOD, - None, - ), - Err(McpToolsListBoundaryError::ProtocolVersionHeaderBodyMismatch) - ); - assert_eq!( - ValidatedMcpToolsListRequest::new( - Some("2025-11-25"), - Some("2025-11-25"), - true, - MCP_TOOLS_LIST_METHOD, - MCP_TOOLS_LIST_METHOD, - None, - ), - Err(McpToolsListBoundaryError::UnsupportedProtocolVersion) - ); - assert_eq!( - ValidatedMcpToolsListRequest::new( - Some(MCP_PROTOCOL_VERSION), - Some(MCP_PROTOCOL_VERSION), - false, - MCP_TOOLS_LIST_METHOD, - MCP_TOOLS_LIST_METHOD, - None, - ), - Err(McpToolsListBoundaryError::MissingClientCapabilities) - ); -} - -#[test] -fn mcp_tools_list_bounds_protocol_metadata_before_cross_field_comparison() { - let oversized_protocol_version = format!("{MCP_PROTOCOL_VERSION}0"); - - for (header, metadata) in [ - (oversized_protocol_version.as_str(), MCP_PROTOCOL_VERSION), - (MCP_PROTOCOL_VERSION, oversized_protocol_version.as_str()), - ] { - assert_eq!( - ValidatedMcpToolsListRequest::new( - Some(header), - Some(metadata), - true, - MCP_TOOLS_LIST_METHOD, - MCP_TOOLS_LIST_METHOD, - None, - ), - Err(McpToolsListBoundaryError::UnsupportedProtocolVersion) - ); - } -} - -#[test] -fn mcp_tools_list_validates_each_method_before_cross_field_comparison() { - let oversized_method = "a".repeat(MAX_MCP_METHOD_NAME_BYTES + 1); - - for (routing_method, body_method) in [ - ("tools list", MCP_TOOLS_LIST_METHOD), - (MCP_TOOLS_LIST_METHOD, "tools list"), - (oversized_method.as_str(), MCP_TOOLS_LIST_METHOD), - (MCP_TOOLS_LIST_METHOD, oversized_method.as_str()), - ] { - assert_eq!( - ValidatedMcpToolsListRequest::new( - Some(MCP_PROTOCOL_VERSION), - Some(MCP_PROTOCOL_VERSION), - true, - routing_method, - body_method, - None, - ), - Err(McpToolsListBoundaryError::InvalidMethod) - ); - } -} - -#[test] -fn mcp_tools_list_request_requires_exact_routing_and_no_unissued_cursor() { - assert_eq!( - ValidatedMcpToolsListRequest::new( - Some(MCP_PROTOCOL_VERSION), - Some(MCP_PROTOCOL_VERSION), - true, - MCP_TOOLS_LIST_METHOD, - "tools/call", - None, - ), - Err(McpToolsListBoundaryError::MethodHeaderBodyMismatch) - ); - assert_eq!( - ValidatedMcpToolsListRequest::new( - Some(MCP_PROTOCOL_VERSION), - Some(MCP_PROTOCOL_VERSION), - true, - "resources/list", - "resources/list", - None, - ), - Err(McpToolsListBoundaryError::UnsupportedMethod) - ); - - for cursor in ["cursor-1", ""] { - assert_eq!( - valid_tools_list_request(Some(cursor)), - Err(McpToolsListBoundaryError::UnsupportedCursor) - ); - } -} - -#[test] -fn mcp_tools_list_request_errors_are_source_free_and_non_echoing() { - let cases = [ - ( - McpToolsListBoundaryError::MissingProtocolVersionHeader, - "MCP protocol version header is required", - ), - ( - McpToolsListBoundaryError::MissingProtocolVersionMetadata, - "MCP request metadata protocol version is required", - ), - ( - McpToolsListBoundaryError::ProtocolVersionHeaderBodyMismatch, - "MCP protocol version header does not match request metadata", - ), - ( - McpToolsListBoundaryError::UnsupportedProtocolVersion, - "unsupported MCP protocol version", - ), - ( - McpToolsListBoundaryError::MissingClientCapabilities, - "MCP request metadata client capabilities are required", - ), - ( - McpToolsListBoundaryError::InvalidMethod, - "MCP method violates the bounded ASCII routing syntax", - ), - ( - McpToolsListBoundaryError::MethodHeaderBodyMismatch, - "MCP method header does not match the request body", - ), - ( - McpToolsListBoundaryError::UnsupportedMethod, - "only MCP tools/list requests can enter the discovery boundary", - ), - ( - McpToolsListBoundaryError::UnsupportedCursor, - "MCP tools/list cursor was not issued by this fixed catalog", - ), - ]; - - for (error, expected) in cases { - assert_eq!(error.to_string(), expected); - assert!(error.source().is_none()); - } -} From 480f6bb7d5811edb0c4b841483866666dab28f71 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:14:25 +0900 Subject: [PATCH 13/50] refactor(ddd): consume MCP adapter contract --- crates/originweave-policy/tests/mcp_route_binding.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/originweave-policy/tests/mcp_route_binding.rs b/crates/originweave-policy/tests/mcp_route_binding.rs index 8e9661af6..2f6bbde9e 100644 --- a/crates/originweave-policy/tests/mcp_route_binding.rs +++ b/crates/originweave-policy/tests/mcp_route_binding.rs @@ -2,11 +2,11 @@ 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_mcp::{MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall}; use originweave_policy::{Decision, DenialReason, evaluate_mcp}; const VALID_INTENT: &str = From 84c3ae1b6ff1b9d60c2a7f05b956278812930448 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:15:18 +0900 Subject: [PATCH 14/50] test(ddd): enforce MCP adapter ownership --- tests/test_repository_contract.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/tests/test_repository_contract.py b/tests/test_repository_contract.py index 057a0011b..8dd6c3e38 100644 --- a/tests/test_repository_contract.py +++ b/tests/test_repository_contract.py @@ -13,7 +13,7 @@ class RepositoryContractTests(unittest.TestCase): """Validate the non-generated repository and governance contract.""" def test_workspace_declares_all_independently_reusable_crates(self) -> None: - """The root workspace must expose every reusable policy kernel.""" + """The root workspace must expose every reusable product boundary.""" data = tomllib.loads((ROOT / "Cargo.toml").read_text(encoding="utf-8")) self.assertEqual( @@ -21,6 +21,7 @@ def test_workspace_declares_all_independently_reusable_crates(self) -> None: { "crates/originweave-core", "crates/originweave-bap", + "crates/originweave-mcp", "crates/originweave-policy", "crates/originweave-destination", "crates/originweave-network", @@ -30,6 +31,25 @@ def test_workspace_declares_all_independently_reusable_crates(self) -> None: }, ) + def test_mcp_adapter_isolated_from_shared_domain_contracts(self) -> None: + """External MCP protocol DTOs and routing must not live in originweave-core.""" + + self.assertFalse((ROOT / "crates/originweave-core/src/mcp.rs").exists()) + mcp_manifest = tomllib.loads( + (ROOT / "crates/originweave-mcp/Cargo.toml").read_text(encoding="utf-8") + ) + self.assertEqual(set(mcp_manifest.get("dependencies", {})), {"originweave-core"}) + + policy_manifest = tomllib.loads( + (ROOT / "crates/originweave-policy/Cargo.toml").read_text(encoding="utf-8") + ) + self.assertIn("originweave-mcp", policy_manifest["dependencies"]) + policy_source = (ROOT / "crates/originweave-policy/src/lib.rs").read_text( + encoding="utf-8" + ) + self.assertNotIn("originweave_core::mcp", policy_source) + self.assertIn("originweave_mcp::ValidatedMcpToolCall", policy_source) + def test_toolchain_is_pinned_to_current_project_baseline(self) -> None: """Reproducible builds require an explicit Rust patch version.""" From 900a4d9597bca1d4fbd23504db704ee5d4be4bd9 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:18:08 +0900 Subject: [PATCH 15/50] build: update lockfile for MCP adapter crate --- Cargo.lock | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 848cb7320..a187092d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,7 +91,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae276ad847ad0a6b3faa4bcb027e9a4614ad0" dependencies = [ "libc", ] @@ -136,7 +136,7 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +checksum = "9ed9a281f7c944f1fddc2fb11170a6660ca1f7b7f98e98f85072a687359528c8" dependencies = [ "block-buffer", "crypto-common", @@ -228,13 +228,13 @@ dependencies = [ name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" +checksum = "521739b2c0d69bf9610d442245a661b62c56f9bf574ef5fd7bf2a6489b78193e" [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2c243f69d781394014ebfe8bbfa0b" dependencies = [ "num-traits", ] @@ -243,7 +243,7 @@ dependencies = [ name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250cfd960262841" dependencies = [ "autocfg", ] @@ -288,6 +288,13 @@ dependencies = [ "originweave-core", ] +[[package]] +name = "originweave-mcp" +version = "0.1.0" +dependencies = [ + "originweave-core", +] + [[package]] name = "originweave-network" version = "0.1.0" @@ -301,6 +308,7 @@ name = "originweave-policy" version = "0.1.0" dependencies = [ "originweave-core", + "originweave-mcp", ] [[package]] From 19ea16a63bf5f1bb442f56cb17f23d43f9051dac Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:22:34 +0900 Subject: [PATCH 16/50] fix(build): restore Cargo registry lock integrity --- Cargo.lock | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a187092d4..445d719a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -91,7 +91,7 @@ checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" name = "cpufeatures" version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae276ad847ad0a6b3faa4bcb027e9a4614ad0" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" dependencies = [ "libc", ] @@ -136,7 +136,7 @@ checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" name = "digest" version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7c944f1fddc2fb11170a6660ca1f7b7f98e98f85072a687359528c8" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -228,13 +228,13 @@ dependencies = [ name = "num-conv" version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521739b2c0d69bf9610d442245a661b62c56f9bf574ef5fd7bf2a6489b78193e" +checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441" [[package]] name = "num-integer" version = "0.1.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2c243f69d781394014ebfe8bbfa0b" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" dependencies = [ "num-traits", ] @@ -243,7 +243,7 @@ dependencies = [ name = "num-traits" version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250cfd960262841" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", ] From 6562deb97b9cfc7ffed8fa21cd316f713d9f15e1 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:22:49 +0900 Subject: [PATCH 17/50] test(ddd): consolidate MCP boundary fitness gate --- tests/test_ddd_boundaries.py | 43 ------------------------------------ 1 file changed, 43 deletions(-) delete mode 100644 tests/test_ddd_boundaries.py diff --git a/tests/test_ddd_boundaries.py b/tests/test_ddd_boundaries.py deleted file mode 100644 index 8da716cf1..000000000 --- a/tests/test_ddd_boundaries.py +++ /dev/null @@ -1,43 +0,0 @@ -"""Architectural fitness tests for bounded-context ownership.""" - -from __future__ import annotations - -import pathlib -import tomllib -import unittest - -ROOT = pathlib.Path(__file__).resolve().parents[1] - - -class DomainBoundaryTests(unittest.TestCase): - """Keep protocol adapters out of the shared domain-contract kernel.""" - - def test_mcp_protocol_contract_has_its_own_adapter_crate(self) -> None: - """MCP routing DTOs belong to the MCP adapter, not originweave-core.""" - - workspace = tomllib.loads((ROOT / "Cargo.toml").read_text(encoding="utf-8")) - self.assertIn("crates/originweave-mcp", workspace["workspace"]["members"]) - self.assertFalse((ROOT / "crates/originweave-core/src/mcp.rs").exists()) - - mcp_manifest = tomllib.loads( - (ROOT / "crates/originweave-mcp/Cargo.toml").read_text(encoding="utf-8") - ) - self.assertEqual( - set(mcp_manifest.get("dependencies", {})), - {"originweave-core"}, - ) - - policy_manifest = tomllib.loads( - (ROOT / "crates/originweave-policy/Cargo.toml").read_text(encoding="utf-8") - ) - self.assertIn("originweave-mcp", policy_manifest["dependencies"]) - - policy_source = (ROOT / "crates/originweave-policy/src/lib.rs").read_text( - encoding="utf-8" - ) - self.assertNotIn("originweave_core::mcp", policy_source) - self.assertIn("originweave_mcp::ValidatedMcpToolCall", policy_source) - - -if __name__ == "__main__": - unittest.main() From 4c08dc8ab0bcf2ce30c09f4f1fa8bd2272cdb327 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:33:10 +0900 Subject: [PATCH 18/50] test(ddd): reject policy-to-MCP dependency inversion --- tests/test_repository_contract.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/tests/test_repository_contract.py b/tests/test_repository_contract.py index 8dd6c3e38..c0933e5f0 100644 --- a/tests/test_repository_contract.py +++ b/tests/test_repository_contract.py @@ -32,23 +32,32 @@ def test_workspace_declares_all_independently_reusable_crates(self) -> None: ) def test_mcp_adapter_isolated_from_shared_domain_contracts(self) -> None: - """External MCP protocol DTOs and routing must not live in originweave-core.""" + """Protocol adapters may depend inward on policy; policy must not depend outward on MCP.""" self.assertFalse((ROOT / "crates/originweave-core/src/mcp.rs").exists()) mcp_manifest = tomllib.loads( (ROOT / "crates/originweave-mcp/Cargo.toml").read_text(encoding="utf-8") ) - self.assertEqual(set(mcp_manifest.get("dependencies", {})), {"originweave-core"}) + self.assertEqual( + set(mcp_manifest.get("dependencies", {})), + {"originweave-core", "originweave-policy"}, + ) policy_manifest = tomllib.loads( (ROOT / "crates/originweave-policy/Cargo.toml").read_text(encoding="utf-8") ) - self.assertIn("originweave-mcp", policy_manifest["dependencies"]) + self.assertEqual(set(policy_manifest.get("dependencies", {})), {"originweave-core"}) policy_source = (ROOT / "crates/originweave-policy/src/lib.rs").read_text( encoding="utf-8" ) - self.assertNotIn("originweave_core::mcp", policy_source) - self.assertIn("originweave_mcp::ValidatedMcpToolCall", policy_source) + self.assertNotIn("originweave_mcp", policy_source) + self.assertNotIn("ValidatedMcpToolCall", policy_source) + + mcp_source = (ROOT / "crates/originweave-mcp/src/lib.rs").read_text( + encoding="utf-8" + ) + self.assertIn("originweave_policy", mcp_source) + self.assertIn("evaluate_mcp", mcp_source) def test_toolchain_is_pinned_to_current_project_baseline(self) -> None: """Reproducible builds require an explicit Rust patch version.""" From f7d505721dd0ef93dbf880018e0c970bc6adf06d Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:36:32 +0900 Subject: [PATCH 19/50] fix(ddd): remove outward MCP dependency from policy --- crates/originweave-policy/Cargo.toml | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/originweave-policy/Cargo.toml b/crates/originweave-policy/Cargo.toml index c56f59222..18732be0b 100644 --- a/crates/originweave-policy/Cargo.toml +++ b/crates/originweave-policy/Cargo.toml @@ -12,7 +12,6 @@ publish = false [dependencies] originweave-core = { path = "../originweave-core" } -originweave-mcp = { path = "../originweave-mcp" } [lints] workspace = true From 3835480837da84c753e327ce4fea2badd77f371a Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:36:45 +0900 Subject: [PATCH 20/50] fix(ddd): make MCP adapter depend inward on policy --- crates/originweave-mcp/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/originweave-mcp/Cargo.toml b/crates/originweave-mcp/Cargo.toml index a86809bda..72aeed352 100644 --- a/crates/originweave-mcp/Cargo.toml +++ b/crates/originweave-mcp/Cargo.toml @@ -12,6 +12,7 @@ publish = false [dependencies] originweave-core = { path = "../originweave-core" } +originweave-policy = { path = "../originweave-policy" } [lints] workspace = true From 04f2ab650d5ee5ebef8a716495239a4a9b61a89b Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:37:10 +0900 Subject: [PATCH 21/50] fix(ddd): keep policy free of MCP adapter types --- crates/originweave-policy/src/lib.rs | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 4c085bd68..972187f70 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -19,7 +19,6 @@ use originweave_core::{ ActionRequest, ApprovalEvidence, ApprovalScope, Capability, ExecutionPurpose, InstructionSource, PolicyContext, RiskClass, RobotsDecision, SecretDelivery, SessionMode, }; -use originweave_mcp::ValidatedMcpToolCall; /// The result of evaluating one typed action request. #[derive(Debug, Clone, PartialEq, Eq)] @@ -41,7 +40,7 @@ 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. + /// A validated external route resolved to a different action than the policy request. McpActionMismatch, /// The session lacks the exact capability required by the action. MissingCapability(Capability), @@ -69,23 +68,6 @@ 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 24d8a3f041de5ca9f8e6859d818ef40c1aa61013 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:37:33 +0900 Subject: [PATCH 22/50] fix(ddd): keep MCP-to-policy bridge in adapter context --- crates/originweave-mcp/src/lib.rs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/crates/originweave-mcp/src/lib.rs b/crates/originweave-mcp/src/lib.rs index 239430598..66694451d 100644 --- a/crates/originweave-mcp/src/lib.rs +++ b/crates/originweave-mcp/src/lib.rs @@ -8,8 +8,29 @@ #![forbid(unsafe_code)] #![deny(missing_docs)] +use originweave_core::{ActionRequest, PolicyContext}; +use originweave_policy::{Decision, DenialReason}; + pub(crate) use originweave_core::{ActionKind, Capability, RiskClass}; mod routing; pub use routing::*; + +/// Evaluate one validated MCP route through the ordinary OriginWeave policy boundary. +/// +/// Route validation proves only protocol integrity. It grants no capability, origin, approval, +/// secret, browser, network, or evidence authority. A route/action mismatch fails closed before +/// the request is delegated to the protocol-independent policy evaluator. +#[must_use] +pub fn evaluate_mcp( + call: &ValidatedMcpToolCall, + request: &ActionRequest, + context: &PolicyContext, +) -> Decision { + if call.action_kind() != request.action() { + return Decision::Deny(DenialReason::McpActionMismatch); + } + + originweave_policy::evaluate(request, context) +} From 7bc28a224f07337098eb0f38b1178713444110c6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:37:55 +0900 Subject: [PATCH 23/50] test(ddd): move MCP-policy binding evidence to adapter context --- .../tests/policy_route_binding.rs | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) create mode 100644 crates/originweave-mcp/tests/policy_route_binding.rs diff --git a/crates/originweave-mcp/tests/policy_route_binding.rs b/crates/originweave-mcp/tests/policy_route_binding.rs new file mode 100644 index 000000000..f9e2fbff4 --- /dev/null +++ b/crates/originweave-mcp/tests/policy_route_binding.rs @@ -0,0 +1,98 @@ +#![allow(clippy::expect_used)] + +use std::collections::BTreeSet; + +use originweave_core::{ + ActionIntentDigest, ActionKind, ActionRequest, ApprovalEvidence, Capability, ExecutionPurpose, + InstructionSource, Origin, PolicyContext, RobotsDecision, SecretDelivery, SessionMode, +}; +use originweave_mcp::{ + MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall, evaluate_mcp, +}; +use originweave_policy::{Decision, DenialReason}; + +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 a6c1fee3f0941c66f56516ba2db029f1ae9a36e0 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:38:07 +0900 Subject: [PATCH 24/50] test(ddd): remove adapter-owned test from policy context --- .../tests/mcp_route_binding.rs | 96 ------------------- 1 file changed, 96 deletions(-) delete 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 deleted file mode 100644 index 2f6bbde9e..000000000 --- a/crates/originweave-policy/tests/mcp_route_binding.rs +++ /dev/null @@ -1,96 +0,0 @@ -#![allow(clippy::expect_used)] - -use std::collections::BTreeSet; - -use originweave_core::{ - ActionIntentDigest, ActionKind, ActionRequest, ApprovalEvidence, Capability, ExecutionPurpose, - InstructionSource, Origin, PolicyContext, RobotsDecision, SecretDelivery, SessionMode, -}; -use originweave_mcp::{MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall}; -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 8300d6208fd2efc0b5f686997d77f8a2b672b475 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:39:39 +0900 Subject: [PATCH 25/50] fix(ddd): align lockfile with adapter dependency direction --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 445d719a2..fadea471a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,7 +57,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" name = "bit-vec" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +checksum = "b71798fca2c1fe1086445a7258a4bc9fd9d8dd28a47c1" dependencies = [ "serde", ] @@ -293,6 +293,7 @@ name = "originweave-mcp" version = "0.1.0" dependencies = [ "originweave-core", + "originweave-policy", ] [[package]] @@ -308,7 +309,6 @@ name = "originweave-policy" version = "0.1.0" dependencies = [ "originweave-core", - "originweave-mcp", ] [[package]] From b89d11049fd9942089f55ea6facb12587af82f44 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 13:40:58 +0900 Subject: [PATCH 26/50] fix(lock): restore verified bit-vec checksum --- Cargo.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index fadea471a..284c2b41a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -57,7 +57,7 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" name = "bit-vec" version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71798fca2c1fe1086445a7258a4bc9fd9d8dd28a47c1" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" dependencies = [ "serde", ] From 9e90c385a92efbcd29ae0d3a14ec510651915504 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 14:17:56 +0900 Subject: [PATCH 27/50] test(ddd): reject MCP vocabulary in policy context --- tests/test_repository_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_repository_contract.py b/tests/test_repository_contract.py index c0933e5f0..b81973386 100644 --- a/tests/test_repository_contract.py +++ b/tests/test_repository_contract.py @@ -52,6 +52,7 @@ def test_mcp_adapter_isolated_from_shared_domain_contracts(self) -> None: ) self.assertNotIn("originweave_mcp", policy_source) self.assertNotIn("ValidatedMcpToolCall", policy_source) + self.assertNotIn("Mcp", policy_source) mcp_source = (ROOT / "crates/originweave-mcp/src/lib.rs").read_text( encoding="utf-8" From e8066ad03ce9634b3ca874443dc4e01581fe24bd Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 14:21:18 +0900 Subject: [PATCH 28/50] fix(ddd): keep MCP route rejection in adapter --- crates/originweave-mcp/src/lib.rs | 20 +++++++++++++------ .../tests/policy_route_binding.rs | 9 +++++---- crates/originweave-policy/src/lib.rs | 2 -- 3 files changed, 19 insertions(+), 12 deletions(-) diff --git a/crates/originweave-mcp/src/lib.rs b/crates/originweave-mcp/src/lib.rs index 66694451d..aecd66658 100644 --- a/crates/originweave-mcp/src/lib.rs +++ b/crates/originweave-mcp/src/lib.rs @@ -9,7 +9,7 @@ #![deny(missing_docs)] use originweave_core::{ActionRequest, PolicyContext}; -use originweave_policy::{Decision, DenialReason}; +use originweave_policy::Decision; pub(crate) use originweave_core::{ActionKind, Capability, RiskClass}; @@ -17,20 +17,28 @@ mod routing; pub use routing::*; +/// A fail-closed rejection owned by the MCP routing boundary rather than policy. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum McpRouteRejection { + /// The validated MCP route resolves to a different action than the typed request. + ActionMismatch, +} + /// Evaluate one validated MCP route through the ordinary OriginWeave policy boundary. /// /// Route validation proves only protocol integrity. It grants no capability, origin, approval, -/// secret, browser, network, or evidence authority. A route/action mismatch fails closed before -/// the request is delegated to the protocol-independent policy evaluator. +/// secret, browser, network, or evidence authority. A route/action mismatch is returned as an +/// MCP-owned rejection before the request reaches policy. Callers may execute only +/// `Ok(Decision::Allow)`; every other result remains non-authorizing. #[must_use] pub fn evaluate_mcp( call: &ValidatedMcpToolCall, request: &ActionRequest, context: &PolicyContext, -) -> Decision { +) -> Result { if call.action_kind() != request.action() { - return Decision::Deny(DenialReason::McpActionMismatch); + return Err(McpRouteRejection::ActionMismatch); } - originweave_policy::evaluate(request, context) + Ok(originweave_policy::evaluate(request, context)) } diff --git a/crates/originweave-mcp/tests/policy_route_binding.rs b/crates/originweave-mcp/tests/policy_route_binding.rs index f9e2fbff4..650753206 100644 --- a/crates/originweave-mcp/tests/policy_route_binding.rs +++ b/crates/originweave-mcp/tests/policy_route_binding.rs @@ -7,7 +7,8 @@ use originweave_core::{ InstructionSource, Origin, PolicyContext, RobotsDecision, SecretDelivery, SessionMode, }; use originweave_mcp::{ - MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall, evaluate_mcp, + MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, McpRouteRejection, ValidatedMcpToolCall, + evaluate_mcp, }; use originweave_policy::{Decision, DenialReason}; @@ -67,7 +68,7 @@ fn matching_mcp_route_enters_the_existing_policy_boundary() { &context(BTreeSet::from([Capability::Observe])), ); - assert_eq!(decision, Decision::Allow); + assert_eq!(decision, Ok(Decision::Allow)); } #[test] @@ -79,7 +80,7 @@ fn mismatched_mcp_route_cannot_be_reinterpreted_as_another_action() { &context(BTreeSet::from([Capability::Navigate])), ); - assert_eq!(decision, Decision::Deny(DenialReason::McpActionMismatch)); + assert_eq!(decision, Err(McpRouteRejection::ActionMismatch)); } #[test] @@ -93,6 +94,6 @@ fn matching_mcp_route_does_not_bypass_existing_policy_denials() { assert_eq!( decision, - Decision::Deny(DenialReason::MissingCapability(Capability::Navigate)) + Ok(Decision::Deny(DenialReason::MissingCapability(Capability::Navigate))) ); } diff --git a/crates/originweave-policy/src/lib.rs b/crates/originweave-policy/src/lib.rs index 972187f70..243ae8ce7 100644 --- a/crates/originweave-policy/src/lib.rs +++ b/crates/originweave-policy/src/lib.rs @@ -40,8 +40,6 @@ pub enum DenialReason { ModePurposeMismatch, /// Page or document content attempted to become a trusted instruction. UntrustedInstructionSource, - /// A validated external 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. From c79a4e9c33a534adabdaef36df82f4fa458afc93 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 14:23:55 +0900 Subject: [PATCH 29/50] style(ddd): apply canonical MCP route-test formatting --- crates/originweave-mcp/tests/policy_route_binding.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/crates/originweave-mcp/tests/policy_route_binding.rs b/crates/originweave-mcp/tests/policy_route_binding.rs index 650753206..b2cdb5cf4 100644 --- a/crates/originweave-mcp/tests/policy_route_binding.rs +++ b/crates/originweave-mcp/tests/policy_route_binding.rs @@ -94,6 +94,8 @@ fn matching_mcp_route_does_not_bypass_existing_policy_denials() { assert_eq!( decision, - Ok(Decision::Deny(DenialReason::MissingCapability(Capability::Navigate))) + Ok(Decision::Deny(DenialReason::MissingCapability( + Capability::Navigate + ))) ); } From b3595ef5656ebdb5aa301d4d2f3e487f6a1f21c8 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Tue, 1 Sep 2026 14:27:33 +0900 Subject: [PATCH 30/50] fix(ddd): rely on Result must-use contract --- crates/originweave-mcp/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/crates/originweave-mcp/src/lib.rs b/crates/originweave-mcp/src/lib.rs index aecd66658..2033f71de 100644 --- a/crates/originweave-mcp/src/lib.rs +++ b/crates/originweave-mcp/src/lib.rs @@ -30,7 +30,6 @@ pub enum McpRouteRejection { /// secret, browser, network, or evidence authority. A route/action mismatch is returned as an /// MCP-owned rejection before the request reaches policy. Callers may execute only /// `Ok(Decision::Allow)`; every other result remains non-authorizing. -#[must_use] pub fn evaluate_mcp( call: &ValidatedMcpToolCall, request: &ActionRequest, From b1a9460f6fb5736fc724a02aa97036d19fcf27eb Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 20:34:45 +0900 Subject: [PATCH 31/50] test(mcp): require modern per-request client metadata --- .../tests/mcp_modern_request_metadata.rs | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 crates/originweave-mcp/tests/mcp_modern_request_metadata.rs diff --git a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs new file mode 100644 index 000000000..cc910bf4e --- /dev/null +++ b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs @@ -0,0 +1,23 @@ +use originweave_mcp::{ + MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall, +}; + +/// MCP 2026-07-28 makes every request self-describing. The current tools/call +/// constructor has no input for the required per-request client identity or +/// capabilities, so a call built only from protocol/routing fields must not be +/// admitted as a fully validated modern request. +#[test] +fn tools_call_without_per_request_client_metadata_fails_closed() { + let result = ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ); + + assert!( + result.is_err(), + "MCP 2026-07-28 tools/call must not validate without per-request client identity and capabilities" + ); +} From 2552c568561496c7685bbf936d7d6e2c8bc264d6 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:11:36 +0900 Subject: [PATCH 32/50] test(mcp): align modern request RED with final spec --- .../tests/mcp_modern_request_metadata.rs | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs index cc910bf4e..bf07980ea 100644 --- a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs +++ b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs @@ -2,12 +2,15 @@ use originweave_mcp::{ MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall, }; -/// MCP 2026-07-28 makes every request self-describing. The current tools/call -/// constructor has no input for the required per-request client identity or -/// capabilities, so a call built only from protocol/routing fields must not be -/// admitted as a fully validated modern request. +/// MCP 2026-07-28 makes the protocol version and client capabilities +/// self-describing on every request. The final protocol keeps `clientInfo` +/// optional and non-authoritative, so absence of client identity must not be an +/// admission failure. The current `tools/call` constructor still has no input +/// for the required request `_meta` protocol version or per-request client +/// capabilities, so a call built only from the transport/routing fields must +/// not be admitted as a fully validated modern request. #[test] -fn tools_call_without_per_request_client_metadata_fails_closed() { +fn tools_call_without_required_per_request_metadata_fails_closed() { let result = ValidatedMcpToolCall::new( MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, @@ -18,6 +21,6 @@ fn tools_call_without_per_request_client_metadata_fails_closed() { assert!( result.is_err(), - "MCP 2026-07-28 tools/call must not validate without per-request client identity and capabilities" + "MCP 2026-07-28 tools/call must not validate without request protocol-version metadata and per-request client capabilities" ); } From 8bbb2ee4f58a985bc277293f925ac6d83725ad67 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 21:12:57 +0900 Subject: [PATCH 33/50] docs(mcp): correct final per-request metadata contract --- docs/traceability/mcp-authority-route.md | 61 ++++++++++++++---------- 1 file changed, 35 insertions(+), 26 deletions(-) diff --git a/docs/traceability/mcp-authority-route.md b/docs/traceability/mcp-authority-route.md index 94f181ed4..a1ab3e3f6 100644 --- a/docs/traceability/mcp-authority-route.md +++ b/docs/traceability/mcp-authority-route.md @@ -1,58 +1,67 @@ # MCP 2026-07-28 authority-route traceability - **`tools/call` capability maturity:** `IMPLEMENTED_ON_PROTECTED_MAIN` -- **`tools/list` capability maturity:** `IMPLEMENTED_ON_ACTIVE_PR` -- **Protected-main owning work:** merged PR #168 `feat(mcp): bind stateless tool routing to typed actions` -- **Active follow-on:** PR #170 `feat(mcp): expose conservative tools list cache contract` +- **`tools/list` capability maturity:** `IMPLEMENTED_ON_PROTECTED_MAIN` +- **Protected-main owning work:** merged PR #168 (`tools/call`) and PR #170 (`tools/list`) +- **Active architecture repair:** PR #272 (`originweave-mcp` adapter boundary) - **Complete MCP adapter status:** `PLANNED` - **Governing decision:** ADR 0107 ## Scope -Protected main at `b05d5acca82b9d916ada2c8e82f59f92a89817e1` contains the bounded Rust control-plane foundation for MCP `2026-07-28` `tools/call` routing that merged through PR #168. 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. +Protected `main@542ca1e9c0a863595b8b6697790005d2471f5413` contains the bounded Rust MCP `2026-07-28` routing and discovery foundation merged through PRs #168 and #170. The current protected-main implementation lives in `originweave-core`: it bounds and syntax-validates attacker-controlled routing fields, maps only the explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from that same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. The `tools/list` boundary requires matching transport/request protocol-version metadata and per-request client-capabilities presence, and returns one complete, private, zero-TTL page with no continuation cursor. -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. +A successful MCP routing value proves protocol integrity only. It grants no capability, origin, approval, secret, browser, tenant, persistence, network, evidence, or ambient execution authority. Browser and policy authority remain in their OriginWeave bounded contexts. -Active PR #170 builds on that protected-main catalog with a conservative typed `tools/list` request/result boundary. Its current branch requires matching MCP protocol metadata, required client-capability presence, bounded and syntax-validated routing/body methods, exact `tools/list` routing, and no caller-supplied cursor because the fixed catalog issues none. Its result is one complete page with zero freshness, private cache scope, and no continuation cursor. This active-PR slice remains non-shipped until it reaches protected main and does not grant any OriginWeave action authority. +PR #272 is an active DDD repair that moves the external MCP protocol surface into `originweave-mcp` while preserving the inward dependency direction: the adapter may consume stable core contracts and the protocol-independent policy API, but core and policy must not depend outward on MCP transport types. The move is active-PR evidence, not protected-main shipment. + +## Final 2026-07-28 per-request envelope + +The final MCP `2026-07-28` request envelope requires `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` on each request. For HTTP, the per-request protocol version must match the `MCP-Protocol-Version` header. Client capabilities are per-request state and must not be inferred from earlier requests. + +`io.modelcontextprotocol/clientInfo` is different: the final revision demoted it to **SHOULD**, not MUST. Requests without `clientInfo` remain valid; when present it is self-reported metadata intended for display, logging, and debugging rather than authorization or security decisions. OriginWeave therefore must not reject an otherwise valid `tools/call` solely because client identity is absent, and must never turn `clientInfo` into browser or policy authority. + +This distinction repairs an earlier active-PR test description that incorrectly grouped client identity with required client capabilities. PR #272 keeps a test-first RED because the current `ValidatedMcpToolCall::new` API still cannot receive the required request `_meta` protocol version or per-request client-capabilities presence, and therefore cannot prove the modern `tools/call` envelope or detect a transport/header-to-request-version mismatch. The production repair remains adapter-local; it must not add MCP concepts to core or policy. ## 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 the bounded `tools/call` foundation now on protected main or by active PR #170: both are reusable control-plane contracts below the complete product adapter. `README.md` and `CHANGELOG.md` distinguish protected-main routing from the active discovery refinement, and ADR 0107 records the protocol/version and authority boundary. +`docs/PRD.md` PRD-INT-004 and the corresponding TRD complete-adapter work remain **Planned**. Protected-main routing/discovery contracts and PR #272's architecture repair are reusable control-plane slices below the complete product adapter. They do not establish a complete MCP server/runtime. -The following remain outside protected main and PR #170 and must not be inferred from either: +The following remain outside the protected-main bounded contract and must not be inferred from it: -- Streamable HTTP transport parsing and header materialization; -- JSON-RPC/HTTP response serialization of the typed discovery page; +- complete Streamable HTTP transport parsing and response serialization; - OAuth and authenticated MCP deployment policy; -- browser-control I/O or BiDi/CDP/WebMCP translation; +- browser-control I/O or WebDriver BiDi/CDP translation; - secret materialization or broker transport; - persistence, durable audit storage, or WARC/PROV export; -- general pagination/subscription state beyond the fixed no-cursor catalog; and +- general pagination/subscription runtime beyond the currently reviewed contracts; and - an OriginWeave Protocol version transition. -## Version boundary +## Version and authority boundary -The protected-main routing foundation and active discovery refinement accept 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. +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. MCP routing metadata and optional client identity remain adapter data, not domain authority. -The reviewed primary source is: +## Executable evidence -Model Context Protocol. (2026, July 28). *Specification: 2026-07-28*. https://modelcontextprotocol.io/specification/2026-07-28 +Protected-main production/test surfaces currently include: -The canonical bibliography remains `docs/doctoring.md`. +- `crates/originweave-core/src/mcp.rs` — deterministic catalog, `tools/call` routing validation, and `tools/list` request/result contracts; +- `crates/originweave-core/tests/mcp_authority_route.rs` — explicit mapping, bounds, malformed inputs, version/method correlation, and public error contracts; +- `crates/originweave-core/tests/mcp_tools_list_cache.rs` — required protocol/client-capabilities metadata, conservative cache/result semantics, method correlation, and cursor rejection; and +- the protocol-independent policy evaluator and route/action preservation tests. -## Executable evidence +Active PR #272 relocates the external-protocol implementation to `crates/originweave-mcp/` and adds `crates/originweave-mcp/tests/mcp_modern_request_metadata.rs` as a RED for the missing required modern `tools/call` request metadata. Exact-current CI/security/review evidence must be regenerated after every branch mutation. Predecessor, protected-main, skipped, status-only, or model evidence is not current-head proof for PR #272. + +## Primary sources -Protected-main PR #168 production/test surfaces include: +Model Context Protocol. (2026, July 28). *The 2026-07-28 specification*. https://modelcontextprotocol.io/specification/2026-07-28 -- `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. +Model Context Protocol. (2026). *Supporting protocol revision 2026-07-28* [TypeScript SDK migration guide]. https://github.com/modelcontextprotocol/typescript-sdk/blob/main/docs/migration/support-2026-07-28.md -Active PR #170 additionally exercises its discovery contract in `crates/originweave-core/tests/mcp_tools_list_cache.rs`, including result/cache semantics, required protocol/client metadata, bounded protocol and method validation, routing correlation, cursor rejection, and public error contracts. +Model Context Protocol. (2026). *2026-07-28 protocol type definitions* [TypeScript source]. https://github.com/modelcontextprotocol/typescript-sdk/blob/main/packages/core-internal/src/types/spec.types.2026-07-28.ts -Exact current-head CI/security/review evidence must be regenerated after every branch mutation. Protected-main evidence proves only the merged `tools/call` foundation; predecessor or protected-main results are not current-head proof for active PR #170. +The canonical broader bibliography remains `docs/doctoring.md`. ## Promotion rule -The bounded `tools/call` routing foundation is already `IMPLEMENTED_ON_PROTECTED_MAIN`. The `tools/list` discovery refinement may change to `IMPLEMENTED_ON_PROTECTED_MAIN` only after PR #170 reaches protected `main` under live governance and exact-head acceptance. Neither promotion makes the complete MCP adapter implemented; each remaining transport/runtime boundary requires its own integrated evidence. +The bounded `tools/call` and `tools/list` foundations are already `IMPLEMENTED_ON_PROTECTED_MAIN`. PR #272 may change the adapter architecture only after its exact current head proves repository-native CI, full owned-production coverage/rustdoc, security gates, required central workflows, and live review governance. Neither that promotion nor the existing protected-main contracts makes the complete MCP adapter implemented; each remaining transport/runtime boundary requires its own integrated evidence. From 34201b35d3c32a948198a6918311498ec5b2ba27 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:35:15 +0900 Subject: [PATCH 34/50] fix(mcp): require modern request metadata for tool calls --- crates/originweave-mcp/src/request.rs | 186 ++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 crates/originweave-mcp/src/request.rs diff --git a/crates/originweave-mcp/src/request.rs b/crates/originweave-mcp/src/request.rs new file mode 100644 index 000000000..d8ae4ae62 --- /dev/null +++ b/crates/originweave-mcp/src/request.rs @@ -0,0 +1,186 @@ +//! MCP 2026-07-28 request-envelope validation for typed tool calls. +//! +//! This adapter layer binds transport protocol metadata to the existing bounded +//! tool-routing validator. It deliberately retains no client identity or +//! capability contents and grants no OriginWeave browser, policy, secret, or +//! evidence authority. + +use std::fmt; + +use crate::{ActionKind, MCP_PROTOCOL_VERSION, routing}; + +/// A deterministic failure while validating one MCP `tools/call` request. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum McpToolBoundaryError { + /// The transport request omitted the required MCP protocol-version header. + MissingProtocolVersionHeader, + /// The structured request metadata omitted the required MCP protocol version. + MissingProtocolVersionMetadata, + /// The transport protocol version disagrees with the structured request metadata. + ProtocolVersionHeaderBodyMismatch, + /// The request names an MCP protocol generation this adapter does not support. + UnsupportedProtocolVersion, + /// The structured request metadata omitted the required client-capabilities object. + MissingClientCapabilities, + /// 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. + 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::MissingProtocolVersionHeader => { + formatter.write_str("MCP protocol version header is required") + } + Self::MissingProtocolVersionMetadata => { + formatter.write_str("MCP request metadata protocol version is required") + } + Self::ProtocolVersionHeaderBodyMismatch => { + formatter.write_str("MCP protocol version header does not match request metadata") + } + Self::UnsupportedProtocolVersion => { + formatter.write_str("unsupported MCP protocol version") + } + Self::MissingClientCapabilities => { + formatter.write_str("MCP request metadata client capabilities are required") + } + 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 => { + 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 {} + +impl From for McpToolBoundaryError { + fn from(error: routing::McpToolBoundaryError) -> Self { + match error { + routing::McpToolBoundaryError::UnsupportedProtocolVersion => { + Self::UnsupportedProtocolVersion + } + routing::McpToolBoundaryError::HeaderBodyMismatch => Self::HeaderBodyMismatch, + routing::McpToolBoundaryError::InvalidMethod => Self::InvalidMethod, + routing::McpToolBoundaryError::UnsupportedMethod => Self::UnsupportedMethod, + routing::McpToolBoundaryError::InvalidToolName => Self::InvalidToolName, + routing::McpToolBoundaryError::UnknownTool => Self::UnknownTool, + } + } +} + +/// An MCP tool call whose required request metadata and routing envelope were validated. +/// +/// The value proves protocol-envelope integrity only. Client capability contents and optional +/// `clientInfo` are deliberately not retained because self-reported client metadata is not an +/// OriginWeave authorization signal. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct ValidatedMcpToolCall { + routed: routing::ValidatedMcpToolCall, +} + +impl ValidatedMcpToolCall { + /// Fail closed for the pre-2026-07-28 constructor shape. + /// + /// This compatibility surface preserves deterministic routing diagnostics for malformed + /// legacy callers, but a syntactically valid route is rejected because this signature cannot + /// prove the required per-request protocol metadata or client-capabilities presence. New + /// adapters must use [`Self::new_with_request_metadata`]. + pub fn new( + protocol_version: &str, + routing_method: &str, + routing_tool_name: &str, + body_method: &str, + body_tool_name: &str, + ) -> Result { + let _ = routing::ValidatedMcpToolCall::new( + protocol_version, + routing_method, + routing_tool_name, + body_method, + body_tool_name, + ) + .map_err(McpToolBoundaryError::from)?; + + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) + } + + /// Validate one MCP 2026-07-28 `tools/call` request envelope. + /// + /// The transport protocol-version header and structured request `_meta` protocol version are + /// both mandatory, are bounded before comparison, must agree exactly, and must equal + /// [`MCP_PROTOCOL_VERSION`]. A trusted structured parser must also attest that the request's + /// client-capabilities object was present. Capability contents and optional `clientInfo` grant + /// no OriginWeave authority and are not retained. After metadata validation, the existing + /// bounded method/tool validator performs the explicit tool-to-action mapping. + pub fn new_with_request_metadata( + protocol_version_header: Option<&str>, + protocol_version_metadata: Option<&str>, + client_capabilities_present: bool, + routing_method: &str, + routing_tool_name: &str, + body_method: &str, + body_tool_name: &str, + ) -> Result { + let protocol_version_header = protocol_version_header + .ok_or(McpToolBoundaryError::MissingProtocolVersionHeader)?; + let protocol_version_metadata = protocol_version_metadata + .ok_or(McpToolBoundaryError::MissingProtocolVersionMetadata)?; + + if protocol_version_header.len() > MCP_PROTOCOL_VERSION.len() + || protocol_version_metadata.len() > MCP_PROTOCOL_VERSION.len() + { + return Err(McpToolBoundaryError::UnsupportedProtocolVersion); + } + if protocol_version_header != protocol_version_metadata { + return Err(McpToolBoundaryError::ProtocolVersionHeaderBodyMismatch); + } + if protocol_version_metadata != MCP_PROTOCOL_VERSION { + return Err(McpToolBoundaryError::UnsupportedProtocolVersion); + } + if !client_capabilities_present { + return Err(McpToolBoundaryError::MissingClientCapabilities); + } + + let routed = routing::ValidatedMcpToolCall::new( + protocol_version_metadata, + routing_method, + routing_tool_name, + body_method, + body_tool_name, + ) + .map_err(McpToolBoundaryError::from)?; + + Ok(Self { routed }) + } + + /// Return the canonical static tool name selected by the explicit mapping. + #[must_use] + pub const fn tool_name(&self) -> &'static str { + self.routed.tool_name() + } + + /// Return the existing OriginWeave typed action selected by this tool. + #[must_use] + pub const fn action_kind(&self) -> ActionKind { + self.routed.action_kind() + } +} From 2bea3e9736845b572ae72311b673356e31966451 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:35:34 +0900 Subject: [PATCH 35/50] refactor(mcp): expose metadata-bound tool call adapter --- crates/originweave-mcp/src/lib.rs | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/originweave-mcp/src/lib.rs b/crates/originweave-mcp/src/lib.rs index 2033f71de..6767e2c17 100644 --- a/crates/originweave-mcp/src/lib.rs +++ b/crates/originweave-mcp/src/lib.rs @@ -13,9 +13,16 @@ use originweave_policy::Decision; pub(crate) use originweave_core::{ActionKind, Capability, RiskClass}; +mod request; mod routing; -pub use routing::*; +pub use request::{McpToolBoundaryError, ValidatedMcpToolCall}; +pub use routing::{ + MAX_MCP_METHOD_NAME_BYTES, MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, MCP_TOOLS_LIST_METHOD, McpCacheScope, McpResultType, + McpToolCatalogEntry, McpToolsListBoundaryError, McpToolsListPage, + ValidatedMcpToolsListRequest, mcp_tools_list_page, supported_mcp_tools, +}; /// A fail-closed rejection owned by the MCP routing boundary rather than policy. #[derive(Debug, Clone, Copy, PartialEq, Eq)] From bff2a7f9726c795aae599632ff71a4c448d7f765 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:35:49 +0900 Subject: [PATCH 36/50] test(mcp): cover mandatory per-request metadata --- .../tests/mcp_modern_request_metadata.rs | 91 ++++++++++++++++--- 1 file changed, 76 insertions(+), 15 deletions(-) diff --git a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs index bf07980ea..611080243 100644 --- a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs +++ b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs @@ -1,26 +1,87 @@ use originweave_mcp::{ - MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, ValidatedMcpToolCall, + MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, McpToolBoundaryError, ValidatedMcpToolCall, }; -/// MCP 2026-07-28 makes the protocol version and client capabilities -/// self-describing on every request. The final protocol keeps `clientInfo` -/// optional and non-authoritative, so absence of client identity must not be an -/// admission failure. The current `tools/call` constructor still has no input -/// for the required request `_meta` protocol version or per-request client -/// capabilities, so a call built only from the transport/routing fields must -/// not be admitted as a fully validated modern request. -#[test] -fn tools_call_without_required_per_request_metadata_fails_closed() { - let result = ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, +fn modern_call( + protocol_version_header: Option<&str>, + protocol_version_metadata: Option<&str>, + client_capabilities_present: bool, +) -> Result { + ValidatedMcpToolCall::new_with_request_metadata( + protocol_version_header, + protocol_version_metadata, + client_capabilities_present, MCP_TOOLS_CALL_METHOD, "originweave.observe", MCP_TOOLS_CALL_METHOD, "originweave.observe", + ) +} + +#[test] +fn legacy_tools_call_shape_fails_closed_without_request_metadata() { + assert_eq!( + ValidatedMcpToolCall::new( + MCP_PROTOCOL_VERSION, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) + ); +} + +#[test] +fn modern_tools_call_requires_both_protocol_version_surfaces() { + assert_eq!( + modern_call(None, Some(MCP_PROTOCOL_VERSION), true), + Err(McpToolBoundaryError::MissingProtocolVersionHeader) + ); + assert_eq!( + modern_call(Some(MCP_PROTOCOL_VERSION), None, true), + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) + ); +} + +#[test] +fn modern_tools_call_bounds_and_cross_checks_protocol_versions() { + let oversized = format!("{MCP_PROTOCOL_VERSION}x"); + + assert_eq!( + modern_call(Some(&oversized), Some(MCP_PROTOCOL_VERSION), true), + Err(McpToolBoundaryError::UnsupportedProtocolVersion) + ); + assert_eq!( + modern_call(Some(MCP_PROTOCOL_VERSION), Some(&oversized), true), + Err(McpToolBoundaryError::UnsupportedProtocolVersion) ); + assert_eq!( + modern_call(Some(MCP_PROTOCOL_VERSION), Some("2025-11-25"), true), + Err(McpToolBoundaryError::ProtocolVersionHeaderBodyMismatch) + ); + assert_eq!( + modern_call(Some("2025-11-25"), Some("2025-11-25"), true), + Err(McpToolBoundaryError::UnsupportedProtocolVersion) + ); +} - assert!( - result.is_err(), - "MCP 2026-07-28 tools/call must not validate without request protocol-version metadata and per-request client capabilities" +#[test] +fn modern_tools_call_requires_per_request_client_capabilities() { + assert_eq!( + modern_call( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + false, + ), + Err(McpToolBoundaryError::MissingClientCapabilities) ); + + let call = modern_call( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, + ) + .expect("required modern request metadata should admit a reviewed tool route"); + assert_eq!(call.tool_name(), "originweave.observe"); } From 24eefaf6ee30637fd0959c9dcc2f77ce314a1ead Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:36:04 +0900 Subject: [PATCH 37/50] test(mcp): bind policy tests to modern request envelope --- crates/originweave-mcp/tests/policy_route_binding.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/crates/originweave-mcp/tests/policy_route_binding.rs b/crates/originweave-mcp/tests/policy_route_binding.rs index b2cdb5cf4..3c5a594ae 100644 --- a/crates/originweave-mcp/tests/policy_route_binding.rs +++ b/crates/originweave-mcp/tests/policy_route_binding.rs @@ -24,8 +24,10 @@ fn intent() -> ActionIntentDigest { } fn validated_call(tool_name: &str) -> ValidatedMcpToolCall { - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + ValidatedMcpToolCall::new_with_request_metadata( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, MCP_TOOLS_CALL_METHOD, tool_name, MCP_TOOLS_CALL_METHOD, From 1aba0a6968e5f4fe3d7c4e8dbe31236978e763c7 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:36:54 +0900 Subject: [PATCH 38/50] test(mcp): preserve bounded routing under modern metadata --- .../tests/mcp_authority_route.rs | 250 ++++++++++-------- 1 file changed, 144 insertions(+), 106 deletions(-) diff --git a/crates/originweave-mcp/tests/mcp_authority_route.rs b/crates/originweave-mcp/tests/mcp_authority_route.rs index e469eb2df..480885892 100644 --- a/crates/originweave-mcp/tests/mcp_authority_route.rs +++ b/crates/originweave-mcp/tests/mcp_authority_route.rs @@ -7,8 +7,10 @@ use originweave_mcp::{ }; fn validate(tool_name: &str) -> Result { - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + ValidatedMcpToolCall::new_with_request_metadata( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, MCP_TOOLS_CALL_METHOD, tool_name, MCP_TOOLS_CALL_METHOD, @@ -16,6 +18,23 @@ fn validate(tool_name: &str) -> Result Result { + ValidatedMcpToolCall::new_with_request_metadata( + Some(MCP_PROTOCOL_VERSION), + Some(MCP_PROTOCOL_VERSION), + true, + routing_method, + routing_tool_name, + body_method, + body_tool_name, + ) +} + #[test] fn supported_mcp_tools_map_to_exact_originweave_actions() -> Result<(), Box> { let cases = [ @@ -91,18 +110,14 @@ 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), @@ -114,10 +129,7 @@ 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(); @@ -125,14 +137,9 @@ fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> Result 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.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()); + assert_eq!(validate(entry.tool_name())?.action_kind(), entry.action_kind()); } for (index, entry) in catalog.iter().enumerate() { @@ -141,29 +148,14 @@ fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> Result assert_ne!(entry.action_kind(), other.action_kind()); } } - assert!( - catalog - .iter() - .all(|entry| entry.action_kind() != ActionKind::LegalConsent) - ); + assert!(catalog.iter().all(|entry| entry.action_kind() != ActionKind::LegalConsent)); 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) - ); +fn modern_route_rejects_header_body_and_method_drift() { assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + modern_route( MCP_TOOLS_CALL_METHOD, "originweave.observe", "tools/list", @@ -172,8 +164,7 @@ fn mcp_route_rejects_protocol_header_body_and_method_drift() { Err(McpToolBoundaryError::HeaderBodyMismatch) ); assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + modern_route( MCP_TOOLS_CALL_METHOD, "originweave.observe", MCP_TOOLS_CALL_METHOD, @@ -182,8 +173,7 @@ fn mcp_route_rejects_protocol_header_body_and_method_drift() { Err(McpToolBoundaryError::HeaderBodyMismatch) ); assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + modern_route( "resources/read", "originweave.observe", "resources/read", @@ -194,123 +184,155 @@ fn mcp_route_rejects_protocol_header_body_and_method_drift() { } #[test] -fn mcp_route_bounds_each_untrusted_method_before_cross_field_comparison() { +fn modern_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); + for (routing_method, body_method) in [ + ("", MCP_TOOLS_CALL_METHOD), + (MCP_TOOLS_CALL_METHOD, ""), + (&oversized_routing, MCP_TOOLS_CALL_METHOD), + (MCP_TOOLS_CALL_METHOD, &oversized_body), + ("tools call", "tools call"), + ] { + assert_eq!( + modern_route( + routing_method, + "originweave.observe", + body_method, + "originweave.observe", + ), + Err(McpToolBoundaryError::InvalidMethod) + ); + } + assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, - "", + modern_route( + &at_limit, "originweave.observe", + &at_limit, + "originweave.observe", + ), + Err(McpToolBoundaryError::UnsupportedMethod) + ); +} + +#[test] +fn modern_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 [ + "", + "originweave legal", + "originweave/observe", + "originweave.관찰", + &oversized, + ] { + assert_eq!(validate(tool_name), Err(McpToolBoundaryError::InvalidToolName)); + } + + assert_eq!(validate(&at_limit), Err(McpToolBoundaryError::UnknownTool)); + assert_eq!( + validate("originweave.legal_consent"), + Err(McpToolBoundaryError::UnknownTool) + ); + assert_eq!( + validate("third_party.arbitrary_javascript"), + Err(McpToolBoundaryError::UnknownTool) + ); + + let oversized_routing = "r".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + let oversized_body = "b".repeat(MAX_MCP_TOOL_NAME_BYTES + 1); + assert_eq!( + modern_route( + MCP_TOOLS_CALL_METHOD, + &oversized_routing, MCP_TOOLS_CALL_METHOD, "originweave.observe", ), - Err(McpToolBoundaryError::InvalidMethod) + Err(McpToolBoundaryError::InvalidToolName) ); assert_eq!( - ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + modern_route( MCP_TOOLS_CALL_METHOD, "originweave.observe", - "", + MCP_TOOLS_CALL_METHOD, + &oversized_body, + ), + Err(McpToolBoundaryError::InvalidToolName) + ); + assert_eq!( + modern_route( + MCP_TOOLS_CALL_METHOD, + "originweave/observe", + MCP_TOOLS_CALL_METHOD, "originweave.observe", ), - Err(McpToolBoundaryError::InvalidMethod) + Err(McpToolBoundaryError::InvalidToolName) ); +} + +#[test] +fn legacy_constructor_preserves_routing_diagnostics_but_never_admits_valid_calls() { assert_eq!( ValidatedMcpToolCall::new( MCP_PROTOCOL_VERSION, - &oversized_routing, + MCP_TOOLS_CALL_METHOD, "originweave.observe", MCP_TOOLS_CALL_METHOD, "originweave.observe", ), - Err(McpToolBoundaryError::InvalidMethod) + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) ); assert_eq!( ValidatedMcpToolCall::new( - MCP_PROTOCOL_VERSION, + "2025-11-25", MCP_TOOLS_CALL_METHOD, "originweave.observe", - &oversized_body, + MCP_TOOLS_CALL_METHOD, "originweave.observe", ), - Err(McpToolBoundaryError::InvalidMethod) + Err(McpToolBoundaryError::UnsupportedProtocolVersion) ); assert_eq!( ValidatedMcpToolCall::new( MCP_PROTOCOL_VERSION, - "tools call", - "originweave.observe", - "tools call", + MCP_TOOLS_CALL_METHOD, "originweave.observe", + MCP_TOOLS_CALL_METHOD, + "originweave.extract", ), - Err(McpToolBoundaryError::InvalidMethod) + Err(McpToolBoundaryError::HeaderBodyMismatch) ); assert_eq!( ValidatedMcpToolCall::new( MCP_PROTOCOL_VERSION, - &at_limit, + "tools call", "originweave.observe", - &at_limit, + "tools call", "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); - 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(&at_limit), Err(McpToolBoundaryError::UnknownTool)); - assert_eq!( - validate("originweave.legal_consent"), - Err(McpToolBoundaryError::UnknownTool) - ); - assert_eq!( - validate("third_party.arbitrary_javascript"), - Err(McpToolBoundaryError::UnknownTool) + Err(McpToolBoundaryError::InvalidMethod) ); -} - -#[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, + "resources/read", + "originweave.observe", + "resources/read", "originweave.observe", ), - Err(McpToolBoundaryError::InvalidToolName) + Err(McpToolBoundaryError::UnsupportedMethod) ); assert_eq!( ValidatedMcpToolCall::new( MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, - "originweave.observe", + "originweave/observe", MCP_TOOLS_CALL_METHOD, - &oversized_body, + "originweave/observe", ), Err(McpToolBoundaryError::InvalidToolName) ); @@ -318,21 +340,37 @@ fn mcp_route_validates_each_untrusted_tool_name_before_cross_field_comparison() ValidatedMcpToolCall::new( MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, - "originweave/observe", + "originweave.unknown", MCP_TOOLS_CALL_METHOD, - "originweave.observe", + "originweave.unknown", ), - Err(McpToolBoundaryError::InvalidToolName) + Err(McpToolBoundaryError::UnknownTool) ); } #[test] fn mcp_boundary_errors_are_deterministic_and_do_not_echo_untrusted_values() { let cases = [ + ( + McpToolBoundaryError::MissingProtocolVersionHeader, + "MCP protocol version header is required", + ), + ( + McpToolBoundaryError::MissingProtocolVersionMetadata, + "MCP request metadata protocol version is required", + ), + ( + McpToolBoundaryError::ProtocolVersionHeaderBodyMismatch, + "MCP protocol version header does not match request metadata", + ), ( McpToolBoundaryError::UnsupportedProtocolVersion, "unsupported MCP protocol version", ), + ( + McpToolBoundaryError::MissingClientCapabilities, + "MCP request metadata client capabilities are required", + ), ( McpToolBoundaryError::HeaderBodyMismatch, "MCP routing headers do not match the request body", From db552ecced6e4994ba221dd83735c1edd7deeae4 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:37:47 +0900 Subject: [PATCH 39/50] docs(mcp): trace metadata-bound tools call repair --- docs/traceability/mcp-authority-route.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/traceability/mcp-authority-route.md b/docs/traceability/mcp-authority-route.md index a1ab3e3f6..14cc2aba8 100644 --- a/docs/traceability/mcp-authority-route.md +++ b/docs/traceability/mcp-authority-route.md @@ -21,7 +21,9 @@ The final MCP `2026-07-28` request envelope requires `io.modelcontextprotocol/pr `io.modelcontextprotocol/clientInfo` is different: the final revision demoted it to **SHOULD**, not MUST. Requests without `clientInfo` remain valid; when present it is self-reported metadata intended for display, logging, and debugging rather than authorization or security decisions. OriginWeave therefore must not reject an otherwise valid `tools/call` solely because client identity is absent, and must never turn `clientInfo` into browser or policy authority. -This distinction repairs an earlier active-PR test description that incorrectly grouped client identity with required client capabilities. PR #272 keeps a test-first RED because the current `ValidatedMcpToolCall::new` API still cannot receive the required request `_meta` protocol version or per-request client-capabilities presence, and therefore cannot prove the modern `tools/call` envelope or detect a transport/header-to-request-version mismatch. The production repair remains adapter-local; it must not add MCP concepts to core or policy. +PR #272 now implements the adapter-local repair for this distinction. The exported `ValidatedMcpToolCall` requires both the HTTP protocol-version surface and structured request protocol version, bounds both before comparison, rejects disagreement or unsupported versions, and requires an attestation that the per-request client-capabilities object was present. It retains neither capabilities contents nor optional client identity. The pre-modern constructor shape remains as a fail-closed compatibility surface: malformed legacy routes still receive deterministic routing diagnostics, while a syntactically valid legacy route cannot become a validated modern tool call because that signature cannot prove required per-request metadata. + +The lower routing validator and reviewed tool-to-`ActionKind` catalog remain internal implementation details of `originweave-mcp`; core and policy receive no MCP request-envelope types. Policy evaluation still consumes only the typed action contract after the adapter has established protocol integrity. ## Product-status reconciliation @@ -50,7 +52,7 @@ Protected-main production/test surfaces currently include: - `crates/originweave-core/tests/mcp_tools_list_cache.rs` — required protocol/client-capabilities metadata, conservative cache/result semantics, method correlation, and cursor rejection; and - the protocol-independent policy evaluator and route/action preservation tests. -Active PR #272 relocates the external-protocol implementation to `crates/originweave-mcp/` and adds `crates/originweave-mcp/tests/mcp_modern_request_metadata.rs` as a RED for the missing required modern `tools/call` request metadata. Exact-current CI/security/review evidence must be regenerated after every branch mutation. Predecessor, protected-main, skipped, status-only, or model evidence is not current-head proof for PR #272. +Active PR #272 relocates the external-protocol implementation to `crates/originweave-mcp/`. Its adapter tests now cover the required modern `tools/call` request metadata, fail-closed legacy constructor, header↔request-version mismatch, missing per-request capabilities, bounded method/tool routing, explicit catalog mapping, and preservation of ordinary policy denials. Exact-current CI/security/review evidence must be regenerated after every branch mutation. Predecessor, protected-main, skipped, status-only, or model evidence is not current-head proof for PR #272. ## Primary sources From c450649d57f2a310bdc038e3e67d0249a349ee66 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Wed, 2 Sep 2026 22:39:08 +0900 Subject: [PATCH 40/50] docs(changelog): record MCP request-envelope hardening --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f747adeae..188dde1b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ All notable changes to OriginWeave are documented in this file. The format follo ### Changed +- Active PR #272 hardens the MCP `2026-07-28` `tools/call` adapter so a validated call requires matching transport/request protocol versions and per-request client-capabilities presence; optional self-reported `clientInfo` is neither required nor retained as authority, and the former constructor shape now fails closed for otherwise valid legacy calls. This is active-PR evidence, not protected-main shipment. - Aligned the hourly product-development branch-coverage toolchain and its one-shot materializer with the reviewed `nightly-2026-08-18` pin, and corrected the official Dependabot Rust-toolchain reference. - Separated logical origin authority from resolved network destination authority; an origin grant no longer implies permission to connect to every resolver result. - Separated resolved-address authorization from direct transport evidence; an approved IP now becomes a usable stream only after the operating system reports the exact requested IP and port. @@ -102,4 +103,4 @@ All notable changes to OriginWeave are documented in this file. The format follo - The hourly product agent has no Git metadata or repository authority. A separate post-verification publisher opens one PR and cannot approve or merge it. - The unprivileged OpenCode user is restricted to loopback egress during model execution, preventing runner-wide allow-listed endpoints from becoming direct source-exfiltration channels. -[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD \ No newline at end of file +[Unreleased]: https://github.com/ContextualWisdomLab/OriginWeave/compare/main...HEAD From bbe6b219a33f78e3b8b1c0166a00e5c34a2ede22 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 00:07:52 +0900 Subject: [PATCH 41/50] test(mcp): expose stdio transport binding gap --- .../tests/mcp_stdio_transport.rs | 74 +++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 crates/originweave-mcp/tests/mcp_stdio_transport.rs diff --git a/crates/originweave-mcp/tests/mcp_stdio_transport.rs b/crates/originweave-mcp/tests/mcp_stdio_transport.rs new file mode 100644 index 000000000..a7947d6d1 --- /dev/null +++ b/crates/originweave-mcp/tests/mcp_stdio_transport.rs @@ -0,0 +1,74 @@ +use originweave_mcp::{ + MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, MCP_TOOLS_LIST_METHOD, McpToolBoundaryError, + McpToolsListBoundaryError, ValidatedMcpToolCall, ValidatedMcpToolsListRequest, +}; + +#[test] +fn modern_stdio_tools_call_admits_body_metadata_without_http_headers() { + let call = ValidatedMcpToolCall::new_for_stdio( + Some(MCP_PROTOCOL_VERSION), + true, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ) + .expect("modern stdio tools/call must not require Streamable HTTP headers"); + + assert_eq!(call.tool_name(), "originweave.observe"); +} + +#[test] +fn modern_stdio_tools_call_still_requires_body_protocol_metadata_and_capabilities() { + assert_eq!( + ValidatedMcpToolCall::new_for_stdio( + None, + true, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::MissingProtocolVersionMetadata) + ); + assert_eq!( + ValidatedMcpToolCall::new_for_stdio( + Some(MCP_PROTOCOL_VERSION), + false, + MCP_TOOLS_CALL_METHOD, + "originweave.observe", + ), + Err(McpToolBoundaryError::MissingClientCapabilities) + ); +} + +#[test] +fn modern_stdio_tools_list_admits_body_metadata_without_http_headers() { + let request = ValidatedMcpToolsListRequest::new_for_stdio( + Some(MCP_PROTOCOL_VERSION), + true, + MCP_TOOLS_LIST_METHOD, + None, + ) + .expect("modern stdio tools/list must not require Streamable HTTP headers"); + + assert_eq!(request.method(), MCP_TOOLS_LIST_METHOD); +} + +#[test] +fn modern_stdio_tools_list_still_requires_body_protocol_metadata_and_capabilities() { + assert_eq!( + ValidatedMcpToolsListRequest::new_for_stdio( + None, + true, + MCP_TOOLS_LIST_METHOD, + None, + ), + Err(McpToolsListBoundaryError::MissingProtocolVersionMetadata) + ); + assert_eq!( + ValidatedMcpToolsListRequest::new_for_stdio( + Some(MCP_PROTOCOL_VERSION), + false, + MCP_TOOLS_LIST_METHOD, + None, + ), + Err(McpToolsListBoundaryError::MissingClientCapabilities) + ); +} From 09ffcccfd91d478120642a4db9bda501655e4533 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 15:20:46 +0900 Subject: [PATCH 42/50] fix(mcp): bind modern stdio requests without HTTP inputs --- crates/originweave-mcp/src/lib.rs | 4 +- crates/originweave-mcp/src/request.rs | 57 ++++++++++++++++++- .../tests/mcp_authority_route.rs | 34 ++++++++--- .../tests/mcp_modern_request_metadata.rs | 8 +-- .../tests/mcp_stdio_transport.rs | 7 +-- 5 files changed, 87 insertions(+), 23 deletions(-) diff --git a/crates/originweave-mcp/src/lib.rs b/crates/originweave-mcp/src/lib.rs index 6767e2c17..1624f27db 100644 --- a/crates/originweave-mcp/src/lib.rs +++ b/crates/originweave-mcp/src/lib.rs @@ -20,8 +20,8 @@ pub use request::{McpToolBoundaryError, ValidatedMcpToolCall}; pub use routing::{ MAX_MCP_METHOD_NAME_BYTES, MAX_MCP_TOOL_NAME_BYTES, MCP_PROTOCOL_VERSION, MCP_TOOLS_CALL_METHOD, MCP_TOOLS_LIST_METHOD, McpCacheScope, McpResultType, - McpToolCatalogEntry, McpToolsListBoundaryError, McpToolsListPage, - ValidatedMcpToolsListRequest, mcp_tools_list_page, supported_mcp_tools, + McpToolCatalogEntry, McpToolsListBoundaryError, McpToolsListPage, ValidatedMcpToolsListRequest, + mcp_tools_list_page, supported_mcp_tools, }; /// A fail-closed rejection owned by the MCP routing boundary rather than policy. diff --git a/crates/originweave-mcp/src/request.rs b/crates/originweave-mcp/src/request.rs index d8ae4ae62..78dc4cf4c 100644 --- a/crates/originweave-mcp/src/request.rs +++ b/crates/originweave-mcp/src/request.rs @@ -140,8 +140,8 @@ impl ValidatedMcpToolCall { body_method: &str, body_tool_name: &str, ) -> Result { - let protocol_version_header = protocol_version_header - .ok_or(McpToolBoundaryError::MissingProtocolVersionHeader)?; + let protocol_version_header = + protocol_version_header.ok_or(McpToolBoundaryError::MissingProtocolVersionHeader)?; let protocol_version_metadata = protocol_version_metadata .ok_or(McpToolBoundaryError::MissingProtocolVersionMetadata)?; @@ -172,6 +172,32 @@ impl ValidatedMcpToolCall { Ok(Self { routed }) } + /// Validate one MCP 2026-07-28 stdio `tools/call` request envelope. + /// + /// Stdio has no HTTP routing headers, so callers provide only request-body protocol metadata, + /// capability presence, method, and tool name. The body values are correlated with themselves + /// inside the existing pure envelope validator only to reuse its bounds and catalog checks; no + /// HTTP header value is accepted, retained, or surfaced as evidence by this constructor. + pub fn new_for_stdio( + protocol_version_metadata: Option<&str>, + client_capabilities_present: bool, + body_method: &str, + body_tool_name: &str, + ) -> Result { + let protocol_version_metadata = protocol_version_metadata + .ok_or(McpToolBoundaryError::MissingProtocolVersionMetadata)?; + + Self::new_with_request_metadata( + Some(protocol_version_metadata), + Some(protocol_version_metadata), + client_capabilities_present, + body_method, + body_tool_name, + body_method, + body_tool_name, + ) + } + /// Return the canonical static tool name selected by the explicit mapping. #[must_use] pub const fn tool_name(&self) -> &'static str { @@ -184,3 +210,30 @@ impl ValidatedMcpToolCall { self.routed.action_kind() } } + +impl routing::ValidatedMcpToolsListRequest { + /// Validate one MCP 2026-07-28 stdio `tools/list` request envelope. + /// + /// Stdio carries the protocol metadata and method in the JSON-RPC request body and has no HTTP + /// routing headers. The body method/version are correlated with themselves inside the existing + /// pure list validator only to reuse its bounded syntax, cache, and cursor checks; callers cannot + /// supply or obtain fabricated HTTP header evidence through this constructor. + pub fn new_for_stdio( + protocol_version_metadata: Option<&str>, + client_capabilities_present: bool, + body_method: &str, + cursor: Option<&str>, + ) -> Result { + let protocol_version_metadata = protocol_version_metadata + .ok_or(routing::McpToolsListBoundaryError::MissingProtocolVersionMetadata)?; + + Self::new( + Some(protocol_version_metadata), + Some(protocol_version_metadata), + client_capabilities_present, + body_method, + body_method, + cursor, + ) + } +} diff --git a/crates/originweave-mcp/tests/mcp_authority_route.rs b/crates/originweave-mcp/tests/mcp_authority_route.rs index 480885892..24d7623fe 100644 --- a/crates/originweave-mcp/tests/mcp_authority_route.rs +++ b/crates/originweave-mcp/tests/mcp_authority_route.rs @@ -110,14 +110,18 @@ 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), @@ -129,7 +133,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(); @@ -137,9 +144,15 @@ fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> Result 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.required_capability(), + expected_action.required_capability() + ); assert_eq!(entry.risk_class(), expected_action.risk_class()); - assert_eq!(validate(entry.tool_name())?.action_kind(), entry.action_kind()); + assert_eq!( + validate(entry.tool_name())?.action_kind(), + entry.action_kind() + ); } for (index, entry) in catalog.iter().enumerate() { @@ -148,7 +161,11 @@ fn mcp_tool_catalog_is_deterministic_complete_and_action_unambiguous() -> Result assert_ne!(entry.action_kind(), other.action_kind()); } } - assert!(catalog.iter().all(|entry| entry.action_kind() != ActionKind::LegalConsent)); + assert!( + catalog + .iter() + .all(|entry| entry.action_kind() != ActionKind::LegalConsent) + ); Ok(()) } @@ -230,7 +247,10 @@ fn modern_route_rejects_unbounded_malformed_and_unmapped_tool_names() { "originweave.관찰", &oversized, ] { - assert_eq!(validate(tool_name), Err(McpToolBoundaryError::InvalidToolName)); + assert_eq!( + validate(tool_name), + Err(McpToolBoundaryError::InvalidToolName) + ); } assert_eq!(validate(&at_limit), Err(McpToolBoundaryError::UnknownTool)); diff --git a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs index 611080243..87d1781a6 100644 --- a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs +++ b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs @@ -77,11 +77,7 @@ fn modern_tools_call_requires_per_request_client_capabilities() { Err(McpToolBoundaryError::MissingClientCapabilities) ); - let call = modern_call( - Some(MCP_PROTOCOL_VERSION), - Some(MCP_PROTOCOL_VERSION), - true, - ) - .expect("required modern request metadata should admit a reviewed tool route"); + let call = modern_call(Some(MCP_PROTOCOL_VERSION), Some(MCP_PROTOCOL_VERSION), true) + .expect("required modern request metadata should admit a reviewed tool route"); assert_eq!(call.tool_name(), "originweave.observe"); } diff --git a/crates/originweave-mcp/tests/mcp_stdio_transport.rs b/crates/originweave-mcp/tests/mcp_stdio_transport.rs index a7947d6d1..3add85e2a 100644 --- a/crates/originweave-mcp/tests/mcp_stdio_transport.rs +++ b/crates/originweave-mcp/tests/mcp_stdio_transport.rs @@ -54,12 +54,7 @@ fn modern_stdio_tools_list_admits_body_metadata_without_http_headers() { #[test] fn modern_stdio_tools_list_still_requires_body_protocol_metadata_and_capabilities() { assert_eq!( - ValidatedMcpToolsListRequest::new_for_stdio( - None, - true, - MCP_TOOLS_LIST_METHOD, - None, - ), + ValidatedMcpToolsListRequest::new_for_stdio(None, true, MCP_TOOLS_LIST_METHOD, None,), Err(McpToolsListBoundaryError::MissingProtocolVersionMetadata) ); assert_eq!( From 80272f18422c9946077ad9bd674f603db8f020da Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Thu, 3 Sep 2026 15:22:06 +0900 Subject: [PATCH 43/50] docs(mcp): trace stdio binding RED and repair --- docs/traceability/mcp-authority-route.md | 31 +++++++++++++++--------- 1 file changed, 19 insertions(+), 12 deletions(-) diff --git a/docs/traceability/mcp-authority-route.md b/docs/traceability/mcp-authority-route.md index 14cc2aba8..45068334e 100644 --- a/docs/traceability/mcp-authority-route.md +++ b/docs/traceability/mcp-authority-route.md @@ -9,22 +9,33 @@ ## Scope -Protected `main@542ca1e9c0a863595b8b6697790005d2471f5413` contains the bounded Rust MCP `2026-07-28` routing and discovery foundation merged through PRs #168 and #170. The current protected-main implementation lives in `originweave-core`: it bounds and syntax-validates attacker-controlled routing fields, maps only the explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from that same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. The `tools/list` boundary requires matching transport/request protocol-version metadata and per-request client-capabilities presence, and returns one complete, private, zero-TTL page with no continuation cursor. +Protected `main@c789b802fc98a8d7fd8c09d9327f36828054d2a1` contains the bounded Rust MCP `2026-07-28` routing and discovery foundation merged through PRs #168 and #170. That protected-main implementation bounds and syntax-validates attacker-controlled routing fields, maps only the explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from that same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. A successful MCP routing value proves protocol integrity only. It grants no capability, origin, approval, secret, browser, tenant, persistence, network, evidence, or ambient execution authority. Browser and policy authority remain in their OriginWeave bounded contexts. -PR #272 is an active DDD repair that moves the external MCP protocol surface into `originweave-mcp` while preserving the inward dependency direction: the adapter may consume stable core contracts and the protocol-independent policy API, but core and policy must not depend outward on MCP transport types. The move is active-PR evidence, not protected-main shipment. +PR #272 is an active DDD repair that moves the external MCP protocol surface into `originweave-mcp` while preserving inward dependency direction: the adapter may consume stable core contracts and the protocol-independent policy API, but core and policy must not depend outward on MCP transport types. The move is active-PR evidence, not protected-main shipment. ## Final 2026-07-28 per-request envelope -The final MCP `2026-07-28` request envelope requires `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` on each request. For HTTP, the per-request protocol version must match the `MCP-Protocol-Version` header. Client capabilities are per-request state and must not be inferred from earlier requests. +The final MCP `2026-07-28` request envelope requires `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` on each request. Client capabilities are per-request state and must not be inferred from earlier requests. `io.modelcontextprotocol/clientInfo` is optional/SHOULD self-reported metadata for display, logging, or debugging; OriginWeave does not use it as browser or policy authority. -`io.modelcontextprotocol/clientInfo` is different: the final revision demoted it to **SHOULD**, not MUST. Requests without `clientInfo` remain valid; when present it is self-reported metadata intended for display, logging, and debugging rather than authorization or security decisions. OriginWeave therefore must not reject an otherwise valid `tools/call` solely because client identity is absent, and must never turn `clientInfo` into browser or policy authority. +Transport binding is explicit. Streamable HTTP requires the request protocol version to agree with the `MCP-Protocol-Version` header and keeps the reviewed routing-header correlation checks. Stdio carries the JSON-RPC request body without those HTTP routing headers, so a valid stdio request must be admitted from its required body metadata rather than from fabricated HTTP evidence. -PR #272 now implements the adapter-local repair for this distinction. The exported `ValidatedMcpToolCall` requires both the HTTP protocol-version surface and structured request protocol version, bounds both before comparison, rejects disagreement or unsupported versions, and requires an attestation that the per-request client-capabilities object was present. It retains neither capabilities contents nor optional client identity. The pre-modern constructor shape remains as a fail-closed compatibility surface: malformed legacy routes still receive deterministic routing diagnostics, while a syntactically valid legacy route cannot become a validated modern tool call because that signature cannot prove required per-request metadata. +PR #272 now exposes separate adapter entry points for those two cases. `ValidatedMcpToolCall::new_with_request_metadata` retains the HTTP header↔body checks. `ValidatedMcpToolCall::new_for_stdio` and `ValidatedMcpToolsListRequest::new_for_stdio` accept only the body protocol version, per-request capabilities-presence attestation, and body routing values. The stdio constructors reuse the same bounded syntax, catalog, and cursor validators internally, but they accept, retain, and expose no HTTP header value. Missing or unsupported protocol metadata, missing capabilities, malformed or unsupported methods, malformed or unknown tools, and unissued cursors remain fail-closed. The lower routing validator and reviewed tool-to-`ActionKind` catalog remain internal implementation details of `originweave-mcp`; core and policy receive no MCP request-envelope types. Policy evaluation still consumes only the typed action contract after the adapter has established protocol integrity. +## Executable RED and repair lineage + +Test-only head `bbe6b219a33f78e3b8b1c0166a00e5c34a2ede22` introduced `crates/originweave-mcp/tests/mcp_stdio_transport.rs` before production constructors existed. Repository-native CI run `33646560232` subsequently acquired hosted runners and produced an executable RED rather than a queue-only signal: + +- Production coverage job `100302670895` failed with Rust `E0599` because `ValidatedMcpToolCall::new_for_stdio` and `ValidatedMcpToolsListRequest::new_for_stdio` did not exist. Six call sites in the stdio contract failed to compile. +- Rust contracts job `100302670660` first passed 154 Python repository-contract tests, then failed `cargo fmt --all --check`. Its canonical rustfmt artifact was `9875815906`, archive SHA-256 `bb5f01d2f6a90f22bc31a7ec34337691b982f73bf56f6064cc01ffb49c024cb6`. + +The causal source repair is commit `09ffcccfd91d478120642a4db9bda501655e4533`. It adds only binding-specific stdio constructors inside `originweave-mcp` and adopts the canonical rustfmt output for files identified by the failed Rust-contract job. It does not move MCP transport authority into core or policy and does not infer browser authorization from protocol success. + +This predecessor RED is durable evidence, but it is not current-head GREEN. Every later commit requires fresh exact-head CI, full owned-production coverage/rustdoc, security gates, and required central review workflows before promotion. + ## Product-status reconciliation `docs/PRD.md` PRD-INT-004 and the corresponding TRD complete-adapter work remain **Planned**. Protected-main routing/discovery contracts and PR #272's architecture repair are reusable control-plane slices below the complete product adapter. They do not establish a complete MCP server/runtime. @@ -32,6 +43,7 @@ The lower routing validator and reviewed tool-to-`ActionKind` catalog remain int The following remain outside the protected-main bounded contract and must not be inferred from it: - complete Streamable HTTP transport parsing and response serialization; +- complete stdio process/runtime framing beyond the request-envelope binding proved here; - OAuth and authenticated MCP deployment policy; - browser-control I/O or WebDriver BiDi/CDP translation; - secret materialization or broker transport; @@ -45,14 +57,9 @@ MCP versioning is independent of the OriginWeave Protocol. A later MCP revision ## Executable evidence -Protected-main production/test surfaces currently include: - -- `crates/originweave-core/src/mcp.rs` — deterministic catalog, `tools/call` routing validation, and `tools/list` request/result contracts; -- `crates/originweave-core/tests/mcp_authority_route.rs` — explicit mapping, bounds, malformed inputs, version/method correlation, and public error contracts; -- `crates/originweave-core/tests/mcp_tools_list_cache.rs` — required protocol/client-capabilities metadata, conservative cache/result semantics, method correlation, and cursor rejection; and -- the protocol-independent policy evaluator and route/action preservation tests. +Protected-main production/test surfaces currently include the deterministic `tools/call`/`tools/list` routing and discovery contracts and protocol-independent policy evaluator. Active PR #272 relocates the external-protocol implementation to `crates/originweave-mcp/` and adds modern HTTP metadata validation plus explicit stdio binding tests for `tools/call` and `tools/list`. -Active PR #272 relocates the external-protocol implementation to `crates/originweave-mcp/`. Its adapter tests now cover the required modern `tools/call` request metadata, fail-closed legacy constructor, header↔request-version mismatch, missing per-request capabilities, bounded method/tool routing, explicit catalog mapping, and preservation of ordinary policy denials. Exact-current CI/security/review evidence must be regenerated after every branch mutation. Predecessor, protected-main, skipped, status-only, or model evidence is not current-head proof for PR #272. +Exact-current CI/security/review evidence must be regenerated after every branch mutation. Predecessor, protected-main, skipped, status-only, model, or cancelled evidence is not current-head proof for PR #272. ## Primary sources From eda00dc5c87ce6164e687d13bea7ce0aa55b909f Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:06:03 +0900 Subject: [PATCH 44/50] test(mcp): remove prohibited metadata expect --- .../originweave-mcp/tests/mcp_modern_request_metadata.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs index 87d1781a6..d551bf782 100644 --- a/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs +++ b/crates/originweave-mcp/tests/mcp_modern_request_metadata.rs @@ -77,7 +77,9 @@ fn modern_tools_call_requires_per_request_client_capabilities() { Err(McpToolBoundaryError::MissingClientCapabilities) ); - let call = modern_call(Some(MCP_PROTOCOL_VERSION), Some(MCP_PROTOCOL_VERSION), true) - .expect("required modern request metadata should admit a reviewed tool route"); - assert_eq!(call.tool_name(), "originweave.observe"); + let call = modern_call(Some(MCP_PROTOCOL_VERSION), Some(MCP_PROTOCOL_VERSION), true); + assert_eq!( + call.as_ref().map(ValidatedMcpToolCall::tool_name), + Ok("originweave.observe") + ); } From c0e587e36045f4128b4c1dd414298c1171abe600 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:06:15 +0900 Subject: [PATCH 45/50] test(mcp): remove prohibited stdio expect --- .../tests/mcp_stdio_transport.rs | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/crates/originweave-mcp/tests/mcp_stdio_transport.rs b/crates/originweave-mcp/tests/mcp_stdio_transport.rs index 3add85e2a..bd15b1f3e 100644 --- a/crates/originweave-mcp/tests/mcp_stdio_transport.rs +++ b/crates/originweave-mcp/tests/mcp_stdio_transport.rs @@ -10,10 +10,12 @@ fn modern_stdio_tools_call_admits_body_metadata_without_http_headers() { true, MCP_TOOLS_CALL_METHOD, "originweave.observe", - ) - .expect("modern stdio tools/call must not require Streamable HTTP headers"); + ); - assert_eq!(call.tool_name(), "originweave.observe"); + assert_eq!( + call.as_ref().map(ValidatedMcpToolCall::tool_name), + Ok("originweave.observe") + ); } #[test] @@ -45,10 +47,14 @@ fn modern_stdio_tools_list_admits_body_metadata_without_http_headers() { true, MCP_TOOLS_LIST_METHOD, None, - ) - .expect("modern stdio tools/list must not require Streamable HTTP headers"); + ); - assert_eq!(request.method(), MCP_TOOLS_LIST_METHOD); + assert_eq!( + request + .as_ref() + .map(ValidatedMcpToolsListRequest::method), + Ok(MCP_TOOLS_LIST_METHOD) + ); } #[test] From cae3e02cd2edc08db06111fb309a5b437c5a6598 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Fri, 4 Sep 2026 22:06:48 +0900 Subject: [PATCH 46/50] test(mcp): cover private routing error diagnostics --- crates/originweave-mcp/src/lib.rs | 42 +++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/crates/originweave-mcp/src/lib.rs b/crates/originweave-mcp/src/lib.rs index 1624f27db..9a3c8008b 100644 --- a/crates/originweave-mcp/src/lib.rs +++ b/crates/originweave-mcp/src/lib.rs @@ -48,3 +48,45 @@ pub fn evaluate_mcp( Ok(originweave_policy::evaluate(request, context)) } + +#[cfg(test)] +mod tests { + use std::error::Error; + + use super::routing::McpToolBoundaryError; + + #[test] + fn private_routing_error_diagnostics_are_total_and_source_free() { + let cases = [ + ( + McpToolBoundaryError::UnsupportedProtocolVersion, + "unsupported MCP protocol version", + ), + ( + McpToolBoundaryError::HeaderBodyMismatch, + "MCP routing headers do not match the request body", + ), + ( + McpToolBoundaryError::InvalidMethod, + "MCP method violates the bounded ASCII routing syntax", + ), + ( + 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, message) in cases { + assert_eq!(error.to_string(), message); + assert!(error.source().is_none()); + } + } +} From fe124e447cad3f679e22337fb6fbdfd135ab3652 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 01:06:13 +0900 Subject: [PATCH 47/50] test(mcp): apply canonical rustfmt diagnostics --- crates/originweave-mcp/tests/mcp_stdio_transport.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/originweave-mcp/tests/mcp_stdio_transport.rs b/crates/originweave-mcp/tests/mcp_stdio_transport.rs index bd15b1f3e..3d7dd5a10 100644 --- a/crates/originweave-mcp/tests/mcp_stdio_transport.rs +++ b/crates/originweave-mcp/tests/mcp_stdio_transport.rs @@ -50,9 +50,7 @@ fn modern_stdio_tools_list_admits_body_metadata_without_http_headers() { ); assert_eq!( - request - .as_ref() - .map(ValidatedMcpToolsListRequest::method), + request.as_ref().map(ValidatedMcpToolsListRequest::method), Ok(MCP_TOOLS_LIST_METHOD) ); } From 55b2f06b038faab50496d33e1affcbb5517af0e5 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:31:19 +0900 Subject: [PATCH 48/50] test(ddd): reconcile MCP workspace contract with PR lifecycle --- tests/test_repository_contract.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_repository_contract.py b/tests/test_repository_contract.py index 00ceb5a12..b12f10d95 100644 --- a/tests/test_repository_contract.py +++ b/tests/test_repository_contract.py @@ -21,6 +21,7 @@ def test_workspace_declares_all_independently_reusable_crates(self) -> None: { "crates/originweave-core", "crates/originweave-bap", + "crates/originweave-mcp", "crates/originweave-policy", "crates/originweave-destination", "crates/originweave-network", From 975492b156803210bd08f5ae27bd78a5da48c693 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:31:31 +0900 Subject: [PATCH 49/50] test(ddd): isolate MCP adapter dependency-direction contract --- tests/test_mcp_adapter_repository_contract.py | 48 +++++++++++++++++++ 1 file changed, 48 insertions(+) create mode 100644 tests/test_mcp_adapter_repository_contract.py diff --git a/tests/test_mcp_adapter_repository_contract.py b/tests/test_mcp_adapter_repository_contract.py new file mode 100644 index 000000000..cd3ca6525 --- /dev/null +++ b/tests/test_mcp_adapter_repository_contract.py @@ -0,0 +1,48 @@ +"""Repository contract for the MCP adapter dependency direction.""" + +from __future__ import annotations + +import pathlib +import tomllib +import unittest + +ROOT = pathlib.Path(__file__).resolve().parents[1] + + +class McpAdapterRepositoryContractTests(unittest.TestCase): + """Keep MCP transport types outside shared domain authority.""" + + def test_mcp_adapter_isolated_from_shared_domain_contracts(self) -> None: + """MCP may depend inward on policy; policy must not depend outward on MCP.""" + + self.assertFalse((ROOT / "crates/originweave-core/src/mcp.rs").exists()) + + mcp_manifest = tomllib.loads( + (ROOT / "crates/originweave-mcp/Cargo.toml").read_text(encoding="utf-8") + ) + self.assertEqual( + set(mcp_manifest.get("dependencies", {})), + {"originweave-core", "originweave-policy"}, + ) + + policy_manifest = tomllib.loads( + (ROOT / "crates/originweave-policy/Cargo.toml").read_text(encoding="utf-8") + ) + self.assertEqual(set(policy_manifest.get("dependencies", {})), {"originweave-core"}) + + policy_source = (ROOT / "crates/originweave-policy/src/lib.rs").read_text( + encoding="utf-8" + ) + self.assertNotIn("originweave_mcp", policy_source) + self.assertNotIn("ValidatedMcpToolCall", policy_source) + self.assertNotIn("Mcp", policy_source) + + mcp_source = (ROOT / "crates/originweave-mcp/src/lib.rs").read_text( + encoding="utf-8" + ) + self.assertIn("originweave_policy", mcp_source) + self.assertIn("evaluate_mcp", mcp_source) + + +if __name__ == "__main__": + unittest.main() From b1cae8ad1cbd8eb6992037c830aea30b9aa436b3 Mon Sep 17 00:00:00 2001 From: Seongho Bae Date: Sat, 5 Sep 2026 08:32:23 +0900 Subject: [PATCH 50/50] docs(ddd): record protected-main MCP reconciliation --- docs/traceability/mcp-authority-route.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/traceability/mcp-authority-route.md b/docs/traceability/mcp-authority-route.md index 45068334e..35c3332db 100644 --- a/docs/traceability/mcp-authority-route.md +++ b/docs/traceability/mcp-authority-route.md @@ -9,12 +9,14 @@ ## Scope -Protected `main@c789b802fc98a8d7fd8c09d9327f36828054d2a1` contains the bounded Rust MCP `2026-07-28` routing and discovery foundation merged through PRs #168 and #170. That protected-main implementation bounds and syntax-validates attacker-controlled routing fields, maps only the explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from that same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. +Protected `main@87c4daa1830bac5a5228b6036752ad5633232085` contains the bounded Rust MCP `2026-07-28` routing and discovery foundation merged through PRs #168 and #170 plus the current repository CI lifecycle authority through #286. That protected-main implementation bounds and syntax-validates attacker-controlled routing fields, maps only the explicit reviewed `originweave.*` catalog to existing typed `ActionKind` values, derives discovery metadata from that same catalog, and rejects route/action mismatch before ordinary deterministic policy evaluation. A successful MCP routing value proves protocol integrity only. It grants no capability, origin, approval, secret, browser, tenant, persistence, network, evidence, or ambient execution authority. Browser and policy authority remain in their OriginWeave bounded contexts. PR #272 is an active DDD repair that moves the external MCP protocol surface into `originweave-mcp` while preserving inward dependency direction: the adapter may consume stable core contracts and the protocol-independent policy API, but core and policy must not depend outward on MCP transport types. The move is active-PR evidence, not protected-main shipment. +The current #272 generation adopts protected #286 non-destructively. Its reconciliation keeps the protected CI/MV3 workflow and lifecycle contract byte-for-byte, retains the MCP workspace membership assertion, and moves the MCP-specific dependency-direction assertions into a focused repository contract instead of overwriting the generic governance contract. The effective PR delta therefore contains no `.github/**` mutation. + ## Final 2026-07-28 per-request envelope The final MCP `2026-07-28` request envelope requires `io.modelcontextprotocol/protocolVersion` and `io.modelcontextprotocol/clientCapabilities` on each request. Client capabilities are per-request state and must not be inferred from earlier requests. `io.modelcontextprotocol/clientInfo` is optional/SHOULD self-reported metadata for display, logging, or debugging; OriginWeave does not use it as browser or policy authority.