Initial commit: hardened DeerFlow factory
Vendored deer-flow upstream (bytedance/deer-flow) plus prompt-injection hardening: - New deerflow.security package: content_delimiter, html_cleaner, sanitizer (8 layers — invisible chars, control chars, symbols, NFC, PUA, tag chars, horizontal whitespace collapse with newline/tab preservation, length cap) - New deerflow.community.searx package: web_search, web_fetch, image_search backed by a private SearX instance, every external string sanitized and wrapped in <<<EXTERNAL_UNTRUSTED_CONTENT>>> delimiters - All native community web providers (ddg_search, tavily, exa, firecrawl, jina_ai, infoquest, image_search) replaced with hard-fail stubs that raise NativeWebToolDisabledError at import time, so a misconfigured tool.use path fails loud rather than silently falling back to unsanitized output - Native client back-doors (jina_client.py, infoquest_client.py) stubbed too - Native-tool tests quarantined under tests/_disabled_native/ (collect_ignore_glob via local conftest.py) - Sanitizer Layer 7 fix: only collapse horizontal whitespace, preserve newlines and tabs so list/table structure survives - Hardened runtime config.yaml references only the searx-backed tools - Factory overlay (backend/) kept in sync with deer-flow tree as a reference / source See HARDENING.md for the full audit trail and verification steps.
This commit is contained in:
85
deer-flow/backend/tests/test_mcp_sync_wrapper.py
Normal file
85
deer-flow/backend/tests/test_mcp_sync_wrapper.py
Normal file
@@ -0,0 +1,85 @@
|
||||
import asyncio
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from langchain_core.tools import StructuredTool
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from deerflow.mcp.tools import _make_sync_tool_wrapper, get_mcp_tools
|
||||
|
||||
|
||||
class MockArgs(BaseModel):
|
||||
x: int = Field(..., description="test param")
|
||||
|
||||
|
||||
def test_mcp_tool_sync_wrapper_generation():
|
||||
"""Test that get_mcp_tools correctly adds a sync func to async-only tools."""
|
||||
|
||||
async def mock_coro(x: int):
|
||||
return f"result: {x}"
|
||||
|
||||
mock_tool = StructuredTool(
|
||||
name="test_tool",
|
||||
description="test description",
|
||||
args_schema=MockArgs,
|
||||
func=None, # Sync func is missing
|
||||
coroutine=mock_coro,
|
||||
)
|
||||
|
||||
mock_client_instance = MagicMock()
|
||||
# Use AsyncMock for get_tools as it's awaited (Fix for Comment 5)
|
||||
mock_client_instance.get_tools = AsyncMock(return_value=[mock_tool])
|
||||
|
||||
with (
|
||||
patch("langchain_mcp_adapters.client.MultiServerMCPClient", return_value=mock_client_instance),
|
||||
patch("deerflow.config.extensions_config.ExtensionsConfig.from_file"),
|
||||
patch("deerflow.mcp.tools.build_servers_config", return_value={"test-server": {}}),
|
||||
patch("deerflow.mcp.tools.get_initial_oauth_headers", new_callable=AsyncMock, return_value={}),
|
||||
):
|
||||
# Run the async function manually with asyncio.run
|
||||
tools = asyncio.run(get_mcp_tools())
|
||||
|
||||
assert len(tools) == 1
|
||||
patched_tool = tools[0]
|
||||
|
||||
# Verify func is now populated
|
||||
assert patched_tool.func is not None
|
||||
|
||||
# Verify it works (sync call)
|
||||
result = patched_tool.func(x=42)
|
||||
assert result == "result: 42"
|
||||
|
||||
|
||||
def test_mcp_tool_sync_wrapper_in_running_loop():
|
||||
"""Test the actual helper function from production code (Fix for Comment 1 & 3)."""
|
||||
|
||||
async def mock_coro(x: int):
|
||||
await asyncio.sleep(0.01)
|
||||
return f"async_result: {x}"
|
||||
|
||||
# Test the real helper function exported from deerflow.mcp.tools
|
||||
sync_func = _make_sync_tool_wrapper(mock_coro, "test_tool")
|
||||
|
||||
async def run_in_loop():
|
||||
# This call should succeed due to ThreadPoolExecutor in the real helper
|
||||
return sync_func(x=100)
|
||||
|
||||
# We run the async function that calls the sync func
|
||||
result = asyncio.run(run_in_loop())
|
||||
assert result == "async_result: 100"
|
||||
|
||||
|
||||
def test_mcp_tool_sync_wrapper_exception_logging():
|
||||
"""Test the actual helper's error logging (Fix for Comment 3)."""
|
||||
|
||||
async def error_coro():
|
||||
raise ValueError("Tool failure")
|
||||
|
||||
sync_func = _make_sync_tool_wrapper(error_coro, "error_tool")
|
||||
|
||||
with patch("deerflow.mcp.tools.logger.error") as mock_log_error:
|
||||
with pytest.raises(ValueError, match="Tool failure"):
|
||||
sync_func()
|
||||
mock_log_error.assert_called_once()
|
||||
# Verify the tool name is in the log message
|
||||
assert "error_tool" in mock_log_error.call_args[0][0]
|
||||
Reference in New Issue
Block a user