mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-07-10 00:18:36 +00:00
* feat(skills): per-user skill isolation (#2905) Implement user-scoped skill storage that isolates custom skills between users while sharing public skills globally. Key changes: - Add UserScopedSkillStorage class for per-user custom skill directories - Introduce get_or_new_user_skill_storage() factory with user_id context - Auth middleware sets effective_user_id for request-scoped storage - Agent/prompt/middleware now use user-scoped storage and prompt cache - Sandbox mounts user-scoped skill directories for search/read tools - Add validate_skill_file_path() to SkillStorage for path security - Migration script supports --all-users bulk migration - Frontend: add editable field to Skill type, error check in enableSkill - All skill categories can be toggled (custom skills default to enabled) - Update skill-creator SKILL.md with isolation-aware instructions Tests: - Add test_user_scoped_skill_storage.py (new) - Update all existing skill tests for user-scoped storage - Update sandbox, client, and router tests * fix(skills): address second-round PR review feedback (#3889) - P1-1: restrict legacy skill mount to users without custom skills - P1-2: fail-closed for _is_disabled_skill_path (OSError → return True) - P2-1: AND-merge global extensions_config skill disabled state - P2-2: atomic write for _skill_states.json (mkstemp + replace) - P2-3: normalize X-DeerFlow-Owner-User-Id in trusted boundary - P2-4: LRU-bounded _enabled_skills_by_config_cache (OrderedDict, maxsize=256) - P2-5: clear global prompt cache on PUBLIC skill toggle - P2-6: invalidate skill caches on client.update_skill * fix(tests): correct tool policy test after merge * fix(skills): use DEFAULT_SKILLS_CONTAINER_PATH in UserScopedSkillStorage The "/mnt/skills" literal in UserScopedSkillStorage.__init__ triggers test_skill_container_path_defaults::test_mnt_skills_literal_is_owned_by_skill_constants_module on CI. Migrate the default to the existing deerflow.constants constant, matching the pattern already used by LocalSkillStorage, SkillStorage, and the durable/tool_error middlewares. --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
90 lines
4.2 KiB
Python
90 lines
4.2 KiB
Python
"""Regression tests for Windows backslash path normalization.
|
|
|
|
Ensures that replace_virtual_paths_in_command and LocalSandbox._resolve_paths_in_command
|
|
return forward-slash paths when the host paths use backslashes (Windows).
|
|
"""
|
|
|
|
from unittest.mock import patch
|
|
|
|
from deerflow.sandbox.local.local_sandbox import LocalSandbox, PathMapping
|
|
from deerflow.sandbox.tools import replace_virtual_paths_in_command
|
|
|
|
# Windows-style thread data with backslash paths
|
|
_WIN_THREAD_DATA = {
|
|
"workspace_path": r"C:\Users\admin\deer-flow\backend\.deer-flow\users\user1\threads\t1\user-data\workspace",
|
|
"uploads_path": r"C:\Users\admin\deer-flow\backend\.deer-flow\users\user1\threads\t1\user-data\uploads",
|
|
"outputs_path": r"C:\Users\admin\deer-flow\backend\.deer-flow\users\user1\threads\t1\user-data\outputs",
|
|
}
|
|
|
|
|
|
class TestReplaceVirtualPathsWindows:
|
|
"""replace_virtual_paths_in_command must normalize backslashes to forward slashes."""
|
|
|
|
def test_user_data_workspace_no_backslash(self) -> None:
|
|
cmd = "cat /mnt/user-data/workspace/data.json"
|
|
result = replace_virtual_paths_in_command(cmd, _WIN_THREAD_DATA)
|
|
assert "\\" not in result, f"Backslash in: {result}"
|
|
|
|
def test_user_data_outputs_no_backslash(self) -> None:
|
|
cmd = "ls /mnt/user-data/outputs/report.html"
|
|
result = replace_virtual_paths_in_command(cmd, _WIN_THREAD_DATA)
|
|
assert "\\" not in result, f"Backslash in: {result}"
|
|
|
|
def test_user_data_subdir_no_backslash(self) -> None:
|
|
cmd = "cat /mnt/user-data/workspace/subdir/file.txt"
|
|
result = replace_virtual_paths_in_command(cmd, _WIN_THREAD_DATA)
|
|
assert "\\" not in result, f"Backslash in: {result}"
|
|
|
|
@patch("deerflow.sandbox.tools._get_skills_host_path", return_value=r"C:\Users\admin\deer-flow\skills")
|
|
@patch("deerflow.sandbox.tools._get_skills_container_path", return_value="/mnt/skills")
|
|
def test_skills_path_no_backslash(self, _mock_container, _mock_host) -> None:
|
|
cmd = "python /mnt/skills/custom/skill/scripts/run.py"
|
|
result = replace_virtual_paths_in_command(cmd, _WIN_THREAD_DATA)
|
|
assert "\\" not in result, f"Backslash in: {result}"
|
|
|
|
|
|
class TestLocalSandboxResolvePathsInCommandWindows:
|
|
"""LocalSandbox._resolve_paths_in_command must normalize backslashes."""
|
|
|
|
def test_custom_mount_no_backslash(self) -> None:
|
|
sandbox = LocalSandbox(
|
|
"test",
|
|
path_mappings=[
|
|
PathMapping(container_path="/mnt/models", local_path=r"C:\Users\admin\models", read_only=True),
|
|
],
|
|
)
|
|
cmd = "cat /mnt/models/weights.bin"
|
|
result = sandbox._resolve_paths_in_command(cmd)
|
|
assert "\\" not in result, f"Backslash in: {result}"
|
|
assert "C:/Users/admin/models/weights.bin" in result
|
|
|
|
def test_user_data_no_backslash(self) -> None:
|
|
sandbox = LocalSandbox(
|
|
"test",
|
|
path_mappings=[
|
|
PathMapping(container_path="/mnt/user-data", local_path=r"C:\Users\admin\data"),
|
|
],
|
|
)
|
|
cmd = "ls /mnt/user-data/workspace/file.txt"
|
|
result = sandbox._resolve_paths_in_command(cmd)
|
|
assert "\\" not in result, f"Backslash in: {result}"
|
|
assert "C:/Users/admin/data/workspace/file.txt" in result
|
|
|
|
def test_acp_workspace_no_backslash(self) -> None:
|
|
"""PR #3889 moved ACP workspace path resolution from
|
|
``replace_virtual_paths_in_command`` to ``LocalSandbox._resolve_paths_in_command``
|
|
via ``PathMapping``. Verify the Windows backslash normalization still
|
|
applies to ACP workspace paths through the new routing — the same
|
|
guarantee ``test_acp_workspace_no_backslash`` (now removed) provided
|
|
for the legacy inline code path.
|
|
"""
|
|
sandbox = LocalSandbox(
|
|
"test",
|
|
path_mappings=[
|
|
PathMapping(container_path="/mnt/acp-workspace", local_path=r"C:\Users\admin\deer-flow\acp-workspace", read_only=False),
|
|
],
|
|
)
|
|
cmd = "cat /mnt/acp-workspace/data.json"
|
|
result = sandbox._resolve_paths_in_command(cmd)
|
|
assert "\\" not in result, f"Backslash in: {result}"
|
|
assert "C:/Users/admin/deer-flow/acp-workspace/data.json" in result
|