Files

807 lines
31 KiB
Python

"""Tests for the ``POST /api/skills/reconcile-custom`` adoption flow.
Covers the scenario where a user drops a skill directory onto disk by hand
(bypassing the upload API) and then calls the reconcile route so it shows up
in the UI.
"""
from __future__ import annotations
import errno
import json
import os
from pathlib import Path
from types import SimpleNamespace
import pytest
from fastapi import HTTPException
from app.gateway.routers import skills as skills_router
from deerflow.community.aio_sandbox import aio_sandbox_provider as aio_provider_module
from deerflow.community.aio_sandbox.remote_backend import RemoteSandboxBackend
from deerflow.config import paths as paths_module
from deerflow.config.extensions_config import get_extensions_config, reset_extensions_config
from deerflow.persistence.skills import MemorySkillStore
from deerflow.sandbox import tools as sandbox_tools
from deerflow.sandbox.local.local_sandbox import LocalSandbox, PathMapping
from deerflow.sandbox.skill_view import (
SKILL_SUPPORT_MANIFEST,
SKILL_VIEW_MARKER,
refresh_skill_sandbox_view,
)
from deerflow.skills.storage.local_skill_storage import LocalSkillStorage
from deerflow.tools.builtins import skill_tools
def _write_skill_on_disk(root: Path, name: str, description: str = "from disk") -> None:
"""Drop a valid SKILL.md into ``<root>/custom/<name>/`` like a manual unzip would."""
skill_dir = root / "custom" / name
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
f"---\nname: {name}\ndescription: {description}\n---\n# {name}\n",
encoding="utf-8",
)
def _fake_request(user_id: str | None = "user-a") -> SimpleNamespace:
user = SimpleNamespace(id=user_id, system_role="user") if user_id else None
return SimpleNamespace(state=SimpleNamespace(user=user))
@pytest.fixture
def skills_root(tmp_path: Path) -> Path:
root = tmp_path / "skills"
(root / "public").mkdir(parents=True)
(root / "custom").mkdir(parents=True)
return root
@pytest.fixture
def patched_storage(
skills_root: Path, monkeypatch: pytest.MonkeyPatch
) -> LocalSkillStorage:
"""Force the router to use a LocalSkillStorage rooted at ``skills_root``."""
storage = LocalSkillStorage(host_path=str(skills_root))
monkeypatch.setattr(
skills_router, "get_or_new_skill_storage", lambda **_: storage
)
async def _noop_refresh() -> None:
return None
monkeypatch.setattr(
skills_router, "refresh_skills_system_prompt_cache_async", _noop_refresh
)
return storage
@pytest.mark.asyncio
async def test_reconcile_adopts_orphan_custom_skill(
skills_root: Path, patched_storage: LocalSkillStorage
):
_write_skill_on_disk(skills_root, "manually-dropped")
store = MemorySkillStore()
result = await skills_router.reconcile_custom_skills(
request=_fake_request("user-a"), config=None, skill_store=store
)
assert result.adopted == ["manually-dropped"]
assert result.already_registered == []
record = await store.get_any("manually-dropped")
assert record is not None
assert record["owner_user_id"] == "user-a"
assert record["published"] is False
@pytest.mark.asyncio
async def test_reconcile_skips_already_registered(
skills_root: Path, patched_storage: LocalSkillStorage
):
_write_skill_on_disk(skills_root, "already-known")
store = MemorySkillStore()
# Pre-existing ownership row — should be left alone.
await store.create(
{"name": "already-known", "owner_user_id": "user-b", "published": True}
)
result = await skills_router.reconcile_custom_skills(
request=_fake_request("user-a"), config=None, skill_store=store
)
assert result.adopted == []
assert result.already_registered == ["already-known"]
# Ownership must not be overwritten.
record = await store.get_any("already-known")
assert record is not None
assert record["owner_user_id"] == "user-b"
assert record["published"] is True
@pytest.mark.asyncio
async def test_reconcile_mixed_adopts_only_orphans(
skills_root: Path, patched_storage: LocalSkillStorage
):
_write_skill_on_disk(skills_root, "orphan-one")
_write_skill_on_disk(skills_root, "orphan-two")
_write_skill_on_disk(skills_root, "registered")
store = MemorySkillStore()
await store.create(
{"name": "registered", "owner_user_id": "user-a", "published": False}
)
result = await skills_router.reconcile_custom_skills(
request=_fake_request("user-a"), config=None, skill_store=store
)
assert sorted(result.adopted) == ["orphan-one", "orphan-two"]
assert result.already_registered == ["registered"]
@pytest.mark.asyncio
async def test_reconcile_with_empty_custom_dir_is_noop(
skills_root: Path, patched_storage: LocalSkillStorage
):
store = MemorySkillStore()
result = await skills_router.reconcile_custom_skills(
request=_fake_request("user-a"), config=None, skill_store=store
)
assert result.adopted == []
assert result.already_registered == []
def _write_availability_skill(skills_root: Path) -> Path:
skill_file = skills_root / "public" / "non-canonical-directory" / "SKILL.md"
skill_file.parent.mkdir(parents=True, exist_ok=True)
skill_file.write_text(
"---\nname: demo-skill\ndescription: Availability test skill\n---\n\n# Distinct skill body\n",
encoding="utf-8",
)
return skill_file
def _availability_request(*, role: str, user_id: str = "test-user") -> SimpleNamespace:
return SimpleNamespace(
state=SimpleNamespace(user=SimpleNamespace(id=user_id, system_role=role)),
)
@pytest.fixture
def isolated_skill_environment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
config_path = tmp_path / "extensions_config.json"
availability_skills_root = tmp_path / "availability-skills"
skill_file = _write_availability_skill(availability_skills_root)
monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(config_path))
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path / "home"))
monkeypatch.setattr(paths_module, "_paths", None)
reset_extensions_config()
try:
yield config_path, LocalSkillStorage(host_path=str(availability_skills_root)), skill_file
finally:
reset_extensions_config()
monkeypatch.setattr(paths_module, "_paths", None)
@pytest.mark.asyncio
async def test_update_public_skill_is_admin_only_and_preserves_raw_config(
isolated_skill_environment,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path, storage, _skill_file = isolated_skill_environment
original = {
"mcpServers": {
"preserved": {
"type": "stdio",
"command": "$MCP_COMMAND",
"args": ["--flag"],
"futureMcpField": {"keep": True},
},
},
"skills": {
"demo-skill": {
"enabled": True,
"display": {
"enabled": True,
"mode": "list",
"resultPath": "hits",
"futureDisplayField": "keep",
},
"futureSkillField": ["keep"],
},
"other-skill": {"enabled": False, "unknown": 7},
},
"futureTopLevel": {"placeholder": "$DO_NOT_RESOLVE", "keep": True},
}
config_path.write_text(json.dumps(original, ensure_ascii=False), encoding="utf-8")
monkeypatch.setenv("MCP_COMMAND", "resolved-secret-command")
monkeypatch.setattr(skills_router, "get_or_new_skill_storage", lambda **_kwargs: storage)
refresh_calls: list[bool] = []
async def _refresh() -> None:
refresh_calls.append(True)
monkeypatch.setattr(skills_router, "refresh_skills_system_prompt_cache_async", _refresh)
replace_calls: list[tuple[object, object]] = []
real_replace = os.replace
def _replace(source, destination) -> None:
replace_calls.append((source, destination))
real_replace(source, destination)
monkeypatch.setattr(skills_router.os, "replace", _replace)
before = config_path.read_bytes()
with pytest.raises(HTTPException) as forbidden:
await skills_router.update_skill(
"demo-skill",
skills_router.SkillUpdateRequest(enabled=False),
_availability_request(role="user"),
config=None,
)
assert forbidden.value.status_code == 403
assert config_path.read_bytes() == before
assert refresh_calls == []
response = await skills_router.update_skill(
"demo-skill",
skills_router.SkillUpdateRequest(enabled=False),
_availability_request(role="admin"),
config=None,
)
persisted = json.loads(config_path.read_text(encoding="utf-8"))
assert response.enabled is False
assert persisted["skills"]["demo-skill"]["enabled"] is False
assert persisted["skills"]["demo-skill"]["display"] == original["skills"]["demo-skill"]["display"]
assert persisted["skills"]["demo-skill"]["futureSkillField"] == ["keep"]
assert persisted["mcpServers"] == original["mcpServers"]
assert persisted["futureTopLevel"] == original["futureTopLevel"]
assert persisted["mcpServers"]["preserved"]["command"] == "$MCP_COMMAND"
assert get_extensions_config().skills["demo-skill"].enabled is False
assert refresh_calls == [True]
config_replaces = [item for item in replace_calls if os.fspath(item[1]) == os.fspath(config_path)]
assert len(config_replaces) == 1
# A second request must re-read the raw document rather than writing a
# stale singleton snapshot.
persisted["concurrentUnknownField"] = {"survives": True}
config_path.write_text(json.dumps(persisted, ensure_ascii=False), encoding="utf-8")
response = await skills_router.update_skill(
"demo-skill",
skills_router.SkillUpdateRequest(enabled=True),
_availability_request(role="admin"),
config=None,
)
persisted = json.loads(config_path.read_text(encoding="utf-8"))
assert response.enabled is True
assert persisted["skills"]["demo-skill"]["enabled"] is True
assert persisted["concurrentUnknownField"] == {"survives": True}
assert refresh_calls == [True, True]
config_replaces = [item for item in replace_calls if os.fspath(item[1]) == os.fspath(config_path)]
assert len(config_replaces) == 2
@pytest.mark.asyncio
async def test_update_custom_skill_allows_owner_but_rejects_other_users(
isolated_skill_environment,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path, storage, _skill_file = isolated_skill_environment
skills_root = storage.get_skills_root_path()
_write_skill_on_disk(skills_root, "owned-skill")
_write_skill_on_disk(skills_root, "other-skill")
_write_skill_on_disk(skills_root, "orphan-skill")
config_path.write_text(
json.dumps(
{
"skills": {
"owned-skill": {"enabled": True},
"other-skill": {"enabled": True},
"orphan-skill": {"enabled": True},
}
}
),
encoding="utf-8",
)
monkeypatch.setattr(skills_router, "get_or_new_skill_storage", lambda **_kwargs: storage)
refresh_calls: list[bool] = []
async def _refresh() -> None:
refresh_calls.append(True)
monkeypatch.setattr(skills_router, "refresh_skills_system_prompt_cache_async", _refresh)
skill_store = MemorySkillStore()
await skill_store.create(
{"name": "owned-skill", "owner_user_id": "user-a", "published": False}
)
await skill_store.create(
{"name": "other-skill", "owner_user_id": "user-b", "published": True}
)
response = await skills_router.update_skill(
"owned-skill",
skills_router.SkillUpdateRequest(enabled=False),
_availability_request(role="user", user_id="user-a"),
config=None,
skill_store=skill_store,
)
assert response.enabled is False
persisted = json.loads(config_path.read_text(encoding="utf-8"))
assert persisted["skills"]["owned-skill"]["enabled"] is False
assert refresh_calls == [True]
response = await skills_router.update_skill(
"owned-skill",
skills_router.SkillUpdateRequest(enabled=True),
_availability_request(role="user", user_id="user-a"),
config=None,
skill_store=skill_store,
)
assert response.enabled is True
persisted = json.loads(config_path.read_text(encoding="utf-8"))
assert persisted["skills"]["owned-skill"]["enabled"] is True
assert refresh_calls == [True, True]
with pytest.raises(HTTPException) as unauthenticated:
await skills_router.update_skill(
"owned-skill",
skills_router.SkillUpdateRequest(enabled=False),
SimpleNamespace(state=SimpleNamespace(user=None)),
config=None,
skill_store=skill_store,
)
assert unauthenticated.value.status_code == 403
for forbidden_name in ("other-skill", "orphan-skill"):
with pytest.raises(HTTPException) as forbidden:
await skills_router.update_skill(
forbidden_name,
skills_router.SkillUpdateRequest(enabled=False),
_availability_request(role="user", user_id="user-a"),
config=None,
skill_store=skill_store,
)
assert forbidden.value.status_code == 403
persisted = json.loads(config_path.read_text(encoding="utf-8"))
assert persisted["skills"]["other-skill"]["enabled"] is True
assert persisted["skills"]["orphan-skill"]["enabled"] is True
assert refresh_calls == [True, True]
admin_response = await skills_router.update_skill(
"orphan-skill",
skills_router.SkillUpdateRequest(enabled=False),
_availability_request(role="admin", user_id="admin-user"),
config=None,
skill_store=skill_store,
)
assert admin_response.enabled is False
persisted = json.loads(config_path.read_text(encoding="utf-8"))
assert persisted["skills"]["orphan-skill"]["enabled"] is False
assert refresh_calls == [True, True, True]
@pytest.mark.asyncio
async def test_update_skill_falls_back_for_bind_mount_ebusy(
isolated_skill_environment,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path, storage, _skill_file = isolated_skill_environment
original = {
"mcpServers": {"server": {"command": "$SECRET", "future": "keep"}},
"skills": {"demo-skill": {"enabled": True, "future": {"keep": True}}},
"futureTopLevel": ["keep"],
}
config_path.write_text(json.dumps(original, ensure_ascii=False), encoding="utf-8")
monkeypatch.setattr(skills_router, "get_or_new_skill_storage", lambda **_kwargs: storage)
refresh_calls: list[bool] = []
async def _refresh() -> None:
refresh_calls.append(True)
monkeypatch.setattr(skills_router, "refresh_skills_system_prompt_cache_async", _refresh)
def _busy_replace(_source, _destination) -> None:
raise OSError(errno.EBUSY, "bind mount")
monkeypatch.setattr(skills_router.os, "replace", _busy_replace)
response = await skills_router.update_skill(
"demo-skill",
skills_router.SkillUpdateRequest(enabled=False),
_availability_request(role="admin"),
config=None,
)
persisted = json.loads(config_path.read_text(encoding="utf-8"))
assert response.enabled is False
assert persisted["skills"]["demo-skill"]["enabled"] is False
assert persisted["skills"]["demo-skill"]["future"] == {"keep": True}
assert persisted["mcpServers"] == original["mcpServers"]
assert persisted["futureTopLevel"] == ["keep"]
assert refresh_calls == [True]
assert list(config_path.parent.glob("tmp*")) == []
@pytest.mark.asyncio
async def test_display_and_delete_state_writes_preserve_other_raw_config(
isolated_skill_environment,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path, storage, _skill_file = isolated_skill_environment
original = {
"mcpServers": {"server": {"command": "$SECRET", "future": "keep"}},
"skills": {
"demo-skill": {
"enabled": False,
"display": {"enabled": False, "mode": "none"},
"futureSkillField": "keep",
},
"other-skill": {"enabled": True, "unknown": 7},
},
"futureTopLevel": {"keep": True},
}
config_path.write_text(json.dumps(original, ensure_ascii=False), encoding="utf-8")
monkeypatch.setattr(skills_router, "get_or_new_skill_storage", lambda **_kwargs: storage)
async def _noop_upsert(*_args, **_kwargs) -> None:
return None
async def _no_adapter(_skill_name: str):
return None
async def _noop_refresh() -> None:
return None
monkeypatch.setattr(skills_router, "_upsert_skill_display_adapter", _noop_upsert)
monkeypatch.setattr(skills_router, "_get_skill_display_adapter", _no_adapter)
monkeypatch.setattr(skills_router, "refresh_skills_system_prompt_cache_async", _noop_refresh)
await skills_router.update_skill_display(
"demo-skill",
skills_router.SkillDisplayUpdateRequest(
display=skills_router.SkillDisplayConfig(enabled=True, mode="list", resultPath="hits"),
),
config=None,
)
persisted = json.loads(config_path.read_text(encoding="utf-8"))
assert persisted["skills"]["demo-skill"]["enabled"] is False
assert persisted["skills"]["demo-skill"]["futureSkillField"] == "keep"
assert persisted["skills"]["demo-skill"]["display"]["resultPath"] == "hits"
assert persisted["mcpServers"] == original["mcpServers"]
assert persisted["futureTopLevel"] == {"keep": True}
skills_router._remove_skill_state_config("demo-skill")
persisted = json.loads(config_path.read_text(encoding="utf-8"))
assert "demo-skill" not in persisted["skills"]
assert persisted["skills"]["other-skill"] == original["skills"]["other-skill"]
assert persisted["mcpServers"] == original["mcpServers"]
assert persisted["futureTopLevel"] == {"keep": True}
def test_skill_list_and_view_hide_offline_skill_and_restore_it(
isolated_skill_environment,
monkeypatch: pytest.MonkeyPatch,
) -> None:
config_path, storage, skill_file = isolated_skill_environment
config_path.write_text(
json.dumps({"mcpServers": {}, "skills": {"demo-skill": {"enabled": False}}}),
encoding="utf-8",
)
import deerflow.skills.storage as storage_module
monkeypatch.setattr(storage_module, "get_or_new_skill_storage", lambda **_kwargs: storage)
monkeypatch.setattr(skill_tools, "_load_owned_skill_names", lambda _user_id: None)
monkeypatch.setattr(skill_tools, "_resolve_effective_user_id", lambda: "test-user")
offline_list = json.loads(skill_tools.skill_list_impl(agent_id=None))
assert all(item["name"] != "demo-skill" for item in offline_list)
assert skill_tools.skill_view_impl("demo-skill", agent_id=None) == "Skill 'demo-skill' not found."
config_path.write_text(
json.dumps({"mcpServers": {}, "skills": {"demo-skill": {"enabled": True}}}),
encoding="utf-8",
)
online_list = json.loads(skill_tools.skill_list_impl(agent_id=None))
assert any(item["name"] == "demo-skill" for item in online_list)
assert skill_tools.skill_view_impl("demo-skill", agent_id=None) == skill_file.read_text(encoding="utf-8")
def test_skill_path_access_gate_uses_discovered_directory_and_fails_closed(
isolated_skill_environment,
) -> None:
config_path, storage, _skill_file = isolated_skill_environment
skill_path = "/mnt/skills/public/non-canonical-directory/bin/run.py"
config_path.write_text(
json.dumps({"mcpServers": {}, "skills": {"demo-skill": {"enabled": False}}}),
encoding="utf-8",
)
with pytest.raises(skill_tools.SkillPathUnavailableError):
skill_tools.require_enabled_skill_path(skill_path, storage=storage)
with pytest.raises(skill_tools.SkillPathUnavailableError):
skill_tools.require_enabled_skill_path("/mnt/skills", storage=storage)
with pytest.raises(skill_tools.SkillPathUnavailableError):
skill_tools.require_enabled_skill_path("/mnt/skills/public", storage=storage)
config_path.write_text(
json.dumps({"mcpServers": {}, "skills": {"demo-skill": {"enabled": True}}}),
encoding="utf-8",
)
skill = skill_tools.require_enabled_skill_path(skill_path, storage=storage)
assert skill.name == "demo-skill"
@pytest.mark.parametrize(
("tool", "args"),
[
(sandbox_tools.bash_tool, (None, "run skill", "python /mnt/skills/public/demo-skill/run.py")),
(sandbox_tools.ls_tool, (None, "list skill", "/mnt/skills/public/demo-skill")),
(sandbox_tools.glob_tool, (None, "find skill files", "**/*.py", "/mnt/skills/public/demo-skill")),
(sandbox_tools.grep_tool, (None, "search skill", "needle", "/mnt/skills/public/demo-skill")),
(sandbox_tools.read_file_tool, (None, "read skill", "/mnt/skills/public/demo-skill/SKILL.md")),
],
)
def test_skill_path_gate_runs_before_local_or_remote_sandbox_dispatch(
tool,
args,
monkeypatch: pytest.MonkeyPatch,
) -> None:
sandbox_calls: list[bool] = []
def _deny(_path: str) -> None:
raise skill_tools.SkillPathUnavailableError("offline")
def _unexpected_sandbox(_runtime):
sandbox_calls.append(True)
raise AssertionError("sandbox dispatch must not occur")
monkeypatch.setattr(sandbox_tools, "_require_enabled_skill_path", _deny)
monkeypatch.setattr(sandbox_tools, "ensure_sandbox_initialized", _unexpected_sandbox)
result = tool.func(*args)
assert result.startswith("Error:")
assert sandbox_calls == []
def _write_named_skill(skills_root: Path, directory: str, name: str, body: str) -> Path:
skill_dir = skills_root / "public" / directory
skill_dir.mkdir(parents=True, exist_ok=True)
(skill_dir / "SKILL.md").write_text(
f"---\nname: {name}\ndescription: Capability test\n---\n\n{body}\n",
encoding="utf-8",
)
return skill_dir
def test_online_skill_view_removes_offline_files_and_keeps_declared_internal_dependency(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
runtime_root = tmp_path / "runtime"
monkeypatch.setenv("DEER_FLOW_HOME", str(runtime_root))
monkeypatch.setattr(paths_module, "_paths", None)
skills_root = tmp_path / "skills"
online_dir = _write_named_skill(skills_root, "online-directory", "online-skill", "online")
offline_dir = _write_named_skill(skills_root, "offline-directory", "offline-skill", "offline")
(online_dir / "online-secret.txt").write_text("ONLINE-CONTENT", encoding="utf-8")
(offline_dir / "offline-secret.txt").write_text("OFFLINE-CONTENT", encoding="utf-8")
support_dir = skills_root / "public" / "task-report-lib"
support_dir.mkdir(parents=True)
(support_dir / "convert_lib.py").write_text("VALUE = 'dependency-present'\n", encoding="utf-8")
(support_dir / SKILL_SUPPORT_MANIFEST).write_text(
json.dumps({"required_by": ["online-skill"]}),
encoding="utf-8",
)
config_path = tmp_path / "extensions_config.json"
config_path.write_text(
json.dumps(
{
"skills": {
"online-skill": {"enabled": True},
"offline-skill": {"enabled": False},
},
}
),
encoding="utf-8",
)
monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(config_path))
storage = LocalSkillStorage(host_path=str(skills_root))
view = refresh_skill_sandbox_view(storage=storage)
assert view.root.parent == runtime_root.resolve()
assert skills_root.resolve() not in view.root.parents
assert (view.root / "public" / "online-directory" / "online-secret.txt").read_text(encoding="utf-8") == "ONLINE-CONTENT"
assert not (view.root / "public" / "offline-directory").exists()
assert (view.root / "public" / "task-report-lib" / "convert_lib.py").is_file()
assert (view.root / SKILL_VIEW_MARKER).read_text(encoding="utf-8") == view.marker_content
assert (offline_dir / "offline-secret.txt").read_text(encoding="utf-8") == "OFFLINE-CONTENT"
config_path.write_text(
json.dumps(
{
"skills": {
"online-skill": {"enabled": False},
"offline-skill": {"enabled": False},
},
}
),
encoding="utf-8",
)
refreshed = refresh_skill_sandbox_view(storage=storage)
assert not (refreshed.root / "public" / "online-directory").exists()
assert not (refreshed.root / "public" / "task-report-lib").exists()
def test_online_skill_view_fails_closed_when_extensions_config_is_invalid(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
runtime_root = tmp_path / "runtime"
monkeypatch.setenv("DEER_FLOW_HOME", str(runtime_root))
monkeypatch.setattr(paths_module, "_paths", None)
skills_root = tmp_path / "skills"
_write_named_skill(skills_root, "online", "online-skill", "online")
config_path = tmp_path / "extensions_config.json"
config_path.write_text(
json.dumps({"skills": {"online-skill": {"enabled": True}}}),
encoding="utf-8",
)
monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(config_path))
storage = LocalSkillStorage(host_path=str(skills_root))
view = refresh_skill_sandbox_view(storage=storage)
assert (view.root / "public" / "online" / "SKILL.md").is_file()
config_path.write_text("{", encoding="utf-8")
with pytest.raises(ValueError, match="not valid JSON"):
refresh_skill_sandbox_view(storage=storage)
assert not (view.root / SKILL_VIEW_MARKER).exists()
assert not any(view.root.iterdir())
def test_local_sandbox_view_blocks_env_and_dynamic_python_bypasses(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("DEER_FLOW_HOME", str(tmp_path / "runtime"))
monkeypatch.setattr(paths_module, "_paths", None)
skills_root = tmp_path / "skills"
online_dir = _write_named_skill(skills_root, "online", "online-skill", "online")
offline_dir = _write_named_skill(skills_root, "offline", "offline-skill", "offline")
(online_dir / "value.txt").write_text("ONLINE-VALUE", encoding="utf-8")
(offline_dir / "value.txt").write_text("OFFLINE-SECRET", encoding="utf-8")
config_path = tmp_path / "extensions_config.json"
config_path.write_text(
json.dumps(
{
"skills": {
"online-skill": {"enabled": True},
"offline-skill": {"enabled": False},
},
}
),
encoding="utf-8",
)
monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", str(config_path))
view = refresh_skill_sandbox_view(storage=LocalSkillStorage(host_path=str(skills_root)))
sandbox = LocalSandbox(
"capability-test",
path_mappings=[PathMapping("/mnt/skills", str(view.root), read_only=True)],
)
monkeypatch.setattr(sandbox_tools, "_get_skills_host_path", lambda: str(view.root))
enabled_command = sandbox_tools.replace_virtual_paths_in_command(
'python -c "from pathlib import Path; print(Path(\'/mnt/skills/public/online/value.txt\').read_text())"',
None,
)
enabled_result = sandbox.execute_command(enabled_command)
assert "ONLINE-VALUE" in enabled_result
env_bypass = sandbox.execute_command(
'python -c "import os; from pathlib import Path; '
"os.environ['SKILL_ROOT']='/'+'mnt'+'/'+'skills'; "
"print(Path(os.environ['SKILL_ROOT'], 'public', 'offline', 'value.txt').read_text())\""
)
dynamic_bypass = sandbox.execute_command(
'python -c "from pathlib import Path; '
"path=Path('/'+'mnt')/'skills'/'public'/'offline'/'value.txt'; "
'print(path.read_text())"'
)
assert "OFFLINE-SECRET" not in env_bypass
assert "OFFLINE-SECRET" not in dynamic_bypass
assert "Exit Code:" in env_bypass
assert "Exit Code:" in dynamic_bypass
def test_local_sandbox_command_environment_hides_backend_skill_locations(
monkeypatch: pytest.MonkeyPatch,
) -> None:
monkeypatch.setenv("DEER_FLOW_SKILLS_PATH", "/canonical/skills")
monkeypatch.setenv("DEER_FLOW_EXTENSIONS_CONFIG_PATH", "/canonical/extensions.json")
monkeypatch.setenv("DEER_FLOW_GATEWAY_PORT", "8001")
monkeypatch.setenv("PYTHONPATH", "/backend/checkout")
monkeypatch.setenv("SAFE_SKILL_API_TOKEN", "kept")
environment = LocalSandbox._model_command_environment()
assert "DEER_FLOW_SKILLS_PATH" not in environment
assert "DEER_FLOW_EXTENSIONS_CONFIG_PATH" not in environment
assert "PYTHONPATH" not in environment
assert environment["DEER_FLOW_GATEWAY_PORT"] == "8001"
assert environment["SAFE_SKILL_API_TOKEN"] == "kept"
def test_aio_sandbox_mounts_and_attests_only_the_online_view(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
view_root = tmp_path / "runtime" / ".sandbox-online-skills"
view_root.mkdir(parents=True)
view = SimpleNamespace(root=view_root, marker_content='{"token":"expected"}')
config = SimpleNamespace(skills=SimpleNamespace(container_path="/mnt/skills"))
monkeypatch.setattr(aio_provider_module, "refresh_skill_sandbox_view", lambda: view)
monkeypatch.setattr(aio_provider_module, "get_app_config", lambda: config)
monkeypatch.setenv("DEER_FLOW_HOST_BASE_DIR", "/host/runtime")
mount = aio_provider_module.AioSandboxProvider._get_skills_mount()
assert mount is not None
assert mount[0].replace("\\", "/").endswith("/host/runtime/.sandbox-online-skills")
assert mount[1:] == ("/mnt/skills", True)
good_sandbox = SimpleNamespace(read_file=lambda path: view.marker_content)
aio_provider_module.AioSandboxProvider._assert_online_skill_view(good_sandbox)
bad_sandbox = SimpleNamespace(read_file=lambda path: "Error: missing")
with pytest.raises(RuntimeError, match="online-only skills view"):
aio_provider_module.AioSandboxProvider._assert_online_skill_view(bad_sandbox)
def test_remote_provisioner_receives_security_sensitive_extra_mounts(
monkeypatch: pytest.MonkeyPatch,
) -> None:
captured: dict[str, object] = {}
class _Response:
def raise_for_status(self) -> None:
return None
def json(self) -> dict[str, str]:
return {"sandbox_url": "http://sandbox:8080"}
def _post(url: str, *, json: dict, timeout: int):
captured.update({"url": url, "json": json, "timeout": timeout})
return _Response()
monkeypatch.setattr("deerflow.community.aio_sandbox.remote_backend.requests.post", _post)
backend = RemoteSandboxBackend("http://provisioner:8002")
backend.create(
"thread-1",
"sandbox-1",
extra_mounts=[("/host/runtime/.sandbox-online-skills", "/mnt/skills", True)],
)
assert captured["json"]["extra_mounts"] == [
{
"host_path": "/host/runtime/.sandbox-online-skills",
"container_path": "/mnt/skills",
"read_only": True,
}
]