88import os
99import shutil
1010from typing import Any , Dict , List , Optional
11+ from urllib .parse import urlparse
1112import zipfile
1213
1314import httpx
1617from agentrun .utils .config import Config
1718from agentrun .utils .log import logger
1819from agentrun .utils .model import BaseModel
20+ from agentrun .utils .ram_signature import get_agentrun_signed_headers
1921
2022from .model import (
2123 McpConfig ,
@@ -128,9 +130,20 @@ class Tool(BaseModel):
128130 tool_type : Optional [str ] = None
129131 """工具类型(MCP/FUNCTIONCALL) / Tool type (MCP/FUNCTIONCALL)"""
130132
133+ create_method : Optional [str ] = None
134+ """工具创建方式 / Tool create method
135+ MCP_REMOTE: 远程 MCP 服务器 / Remote MCP server
136+ MCP_LOCAL: 本地 MCP 标准输入输出 / Local MCP stdio
137+ MCP_BUNDLE: MCP 打包部署 / MCP bundle deployment
138+ CODE_PACKAGE: 代码包部署 / Code package deployment
139+ OPENAPI_IMPORT: OpenAPI 导入 / OpenAPI import
140+ """
141+
131142 version_id : Optional [str ] = None
132143 """版本 ID / Version ID"""
133144
145+ _RAM_DATA_DOMAINS = ("agentrun-data" , "funagent-data-pre" )
146+
134147 @classmethod
135148 def __get_client (cls , config : Optional [Config ] = None ):
136149 from .client import ToolClient
@@ -238,20 +251,36 @@ async def list_tools_async(
238251 self , "mcp_config.session_affinity" , "MCP_SSE"
239252 )
240253
254+ # MCP_REMOTE + proxy_enabled=false 时直连外部服务,不走 RAM 鉴权
255+ # Only skip RAM auth for MCP_REMOTE with proxy disabled (direct external connection)
256+ is_mcp_remote_without_proxy = (
257+ self .create_method == "MCP_REMOTE"
258+ and not pydash .get (self , "mcp_config.proxy_enabled" , False )
259+ )
260+
241261 cfg = Config .with_configs (config )
242262 session = ToolMCPSession (
243263 endpoint = mcp_endpoint ,
244264 session_affinity = session_affinity ,
245265 headers = cfg .get_headers (),
266+ config = cfg ,
267+ use_ram_auth = not is_mcp_remote_without_proxy ,
246268 )
247269 return await session .list_tools_async ()
248270
249271 elif tool_type == ToolType .FUNCTIONCALL :
250272 from .api .openapi import ToolOpenAPIClient
251273
274+ # OPENAPI_IMPORT 时 server 是外部服务,不走 RAM 鉴权
275+ # Skip RAM auth for OPENAPI_IMPORT since the server is an external service
276+ is_openapi_import = self .create_method == "OPENAPI_IMPORT"
277+
278+ cfg = Config .with_configs (config )
252279 openapi_client = ToolOpenAPIClient (
253280 protocol_spec = self .protocol_spec ,
254281 fallback_server_url = self ._get_functioncall_server_url (config ),
282+ config = cfg ,
283+ use_ram_auth = not is_openapi_import ,
255284 )
256285 return await openapi_client .list_tools_async ()
257286
@@ -290,11 +319,20 @@ async def call_tool_async(
290319 self , "mcp_config.session_affinity" , "MCP_SSE"
291320 )
292321
322+ # MCP_REMOTE + proxy_enabled=false 时直连外部服务,不走 RAM 鉴权
323+ # Only skip RAM auth for MCP_REMOTE with proxy disabled (direct external connection)
324+ is_mcp_remote_without_proxy = (
325+ self .create_method == "MCP_REMOTE"
326+ and not pydash .get (self , "mcp_config.proxy_enabled" , False )
327+ )
328+
293329 cfg = Config .with_configs (config )
294330 session = ToolMCPSession (
295331 endpoint = mcp_endpoint ,
296332 session_affinity = session_affinity ,
297333 headers = cfg .get_headers (),
334+ config = cfg ,
335+ use_ram_auth = not is_mcp_remote_without_proxy ,
298336 )
299337 result = await session .call_tool_async (name , arguments )
300338 logger .debug ("invoke tool %s got result %s" , name , result )
@@ -303,18 +341,101 @@ async def call_tool_async(
303341 elif tool_type == ToolType .FUNCTIONCALL :
304342 from .api .openapi import ToolOpenAPIClient
305343
344+ # OPENAPI_IMPORT 时 server 是外部服务,不走 RAM 鉴权
345+ # Skip RAM auth for OPENAPI_IMPORT since the server is an external service
346+ is_openapi_import = self .create_method == "OPENAPI_IMPORT"
347+
306348 cfg = Config .with_configs (config )
307349 openapi_client = ToolOpenAPIClient (
308350 protocol_spec = self .protocol_spec ,
309351 headers = cfg .get_headers (),
310352 fallback_server_url = self ._get_functioncall_server_url (config ),
353+ config = cfg ,
354+ use_ram_auth = not is_openapi_import ,
311355 )
312356 result = await openapi_client .call_tool_async (name , arguments )
313357 logger .debug ("invoke tool %s got result %s" , name , result )
314358 return result
315359
316360 raise ValueError (f"Unsupported tool type: { self .tool_type } " )
317361
362+ def _use_ram_auth (self , config : Optional [Config ] = None ) -> bool :
363+ """是否使用 RAM 签名鉴权(配置了 AK/SK 时使用)。
364+ Whether to use RAM signature authentication (when AK/SK is configured).
365+ """
366+ cfg = Config .with_configs (config )
367+ return bool (cfg .get_access_key_id () and cfg .get_access_key_secret ())
368+
369+ def _get_ram_data_endpoint (
370+ self , url : str , config : Optional [Config ] = None
371+ ) -> str :
372+ """返回 RAM 鉴权用的 data endpoint(仅当 agentrun-data / funagent-data-pre 域名时在 host 前加 -ram)。
373+ Return RAM-authenticated endpoint (add -ram prefix for agentrun-data / funagent-data-pre domains).
374+ """
375+ parsed = urlparse (url )
376+ if not parsed .netloc or not any (
377+ f".{ domain } ." in parsed .netloc for domain in self ._RAM_DATA_DOMAINS
378+ ):
379+ return url
380+ parts = parsed .netloc .split ("." , 1 )
381+ if len (parts ) != 2 :
382+ return url
383+ ram_netloc = parts [0 ] + "-ram." + parts [1 ]
384+
385+ from urllib .parse import urlunparse
386+
387+ return urlunparse ((
388+ parsed .scheme ,
389+ ram_netloc ,
390+ parsed .path ,
391+ parsed .params ,
392+ parsed .query ,
393+ parsed .fragment ,
394+ ))
395+
396+ def _get_auth_headers (
397+ self , url : str , config : Optional [Config ] = None
398+ ) -> Dict [str , str ]:
399+ """获取认证请求头,支持 RAM 签名。
400+ Get authentication headers with RAM signature support.
401+
402+ Args:
403+ url: 请求 URL / Request URL
404+ config: 配置对象 / Configuration object
405+
406+ Returns:
407+ Dict[str, str]: 包含认证信息的请求头 / Headers with authentication
408+ """
409+ cfg = Config .with_configs (config )
410+ headers = cfg .get_headers ()
411+
412+ if self ._use_ram_auth (cfg ):
413+ # 使用 RAM 端点
414+ ram_url = self ._get_ram_data_endpoint (url , cfg )
415+ try :
416+ signed = get_agentrun_signed_headers (
417+ url = ram_url ,
418+ method = "GET" ,
419+ access_key_id = cfg .get_access_key_id (),
420+ access_key_secret = cfg .get_access_key_secret (),
421+ security_token = cfg .get_security_token () or None ,
422+ region = cfg .get_region_id (),
423+ product = "agentrun" ,
424+ body = None ,
425+ )
426+ headers = {
427+ ** signed ,
428+ ** headers ,
429+ }
430+ logger .debug (
431+ "using RAM signature for skill download to %s" ,
432+ ram_url [:80 ] + "..." if len (ram_url ) > 80 else ram_url ,
433+ )
434+ except ValueError as e :
435+ logger .warning ("RAM signing skipped (missing AK/SK): %s" , e )
436+
437+ return headers
438+
318439 def _get_skill_download_url (
319440 self , config : Optional [Config ] = None
320441 ) -> Optional [str ]:
@@ -376,7 +497,7 @@ async def download_skill_async(
376497 logger .debug ("downloading skill from %s to %s" , download_url , skill_dir )
377498
378499 cfg = Config .with_configs (config )
379- headers = cfg . get_headers ( )
500+ headers = self . _get_auth_headers ( download_url , cfg )
380501
381502 async with httpx .AsyncClient (
382503 timeout = 300 , follow_redirects = True
0 commit comments