mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 21:26:39 +00:00
* feat(skills): add guarded evolution host APIs * docs(agents): avoid growing saturated guidance chains * fix(skills): address mutation host review findings * fix(skills): order name fence before projection lock * fix(skills): preserve composite toggle lock order * fix(skills): address catalog and pagination review feedback * docs: keep subagent guidance within merged instruction budget --------- Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
73 lines
3.4 KiB
Python
73 lines
3.4 KiB
Python
from unittest.mock import AsyncMock
|
|
|
|
import pytest
|
|
|
|
from deerflow.config.app_config import AppConfig
|
|
from deerflow.skills.mutations.assets import PackageFile, PackageSnapshot
|
|
from deerflow.skills.security_scanner import ScanResult
|
|
|
|
CONTENT = b"---\nname: example\ndescription: Fine\n---\nHelpful instructions.\n"
|
|
|
|
|
|
def configuration(enabled=True):
|
|
return AppConfig.model_validate({"sandbox": {"use": "deerflow.sandbox.local:LocalSandboxProvider"}, "skill_scan": {"enabled": enabled}, "skill_evolution": {"security_fail_closed": False}})
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_host_check_never_treats_disabled_scanner_as_allow(monkeypatch):
|
|
from deerflow.skills.mutations.scanner import CandidateScanner
|
|
|
|
model = AsyncMock(return_value=ScanResult("allow", "fine"))
|
|
monkeypatch.setattr("deerflow.skills.mutations.scanner.scan_skill_content", model)
|
|
result = await CandidateScanner(lambda: configuration(False)).scan(PackageSnapshot((PackageFile("SKILL.md", CONTENT, False),)), "example")
|
|
assert result.decision == "unavailable"
|
|
model.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_host_scan_covers_support_files_and_forces_fail_closed(monkeypatch):
|
|
from deerflow.skills.mutations.scanner import CandidateScanner
|
|
|
|
model = AsyncMock(return_value=ScanResult("allow", "fine"))
|
|
monkeypatch.setattr("deerflow.skills.mutations.scanner.scan_skill_content", model)
|
|
scanner = CandidateScanner(configuration)
|
|
package = PackageSnapshot((PackageFile("SKILL.md", CONTENT, False), PackageFile("references/help.md", b"Supporting advice", False)))
|
|
result = await scanner.scan(package, "example")
|
|
assert result.decision == "allow"
|
|
assert result.policy_version == await scanner.policy_version()
|
|
assert model.await_count == 2
|
|
assert all(call.kwargs["app_config"].skill_evolution.security_fail_closed for call in model.await_args_list)
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_static_block_prevents_model_scan(monkeypatch):
|
|
from deerflow.skills.mutations.scanner import CandidateScanner
|
|
|
|
model = AsyncMock(return_value=ScanResult("allow", "fine"))
|
|
monkeypatch.setattr("deerflow.skills.mutations.scanner.scan_skill_content", model)
|
|
package = PackageSnapshot((PackageFile("SKILL.md", CONTENT, False), PackageFile("scripts/run.py", b"eval(input())", True)))
|
|
result = await CandidateScanner(configuration).scan(package, "example")
|
|
assert result.decision == "reject"
|
|
model.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_scanner_resource_limit_is_not_partial_approval(monkeypatch):
|
|
from deerflow.skills.mutations.scanner import CandidateScanner
|
|
|
|
model = AsyncMock(return_value=ScanResult("allow", "fine"))
|
|
monkeypatch.setattr("deerflow.skills.mutations.scanner.scan_skill_content", model)
|
|
package = PackageSnapshot((PackageFile("SKILL.md", CONTENT, False), PackageFile("notes.txt", b"x" * (128 * 1024 + 1), False)))
|
|
result = await CandidateScanner(configuration).scan(package, "example")
|
|
assert result.decision == "unavailable"
|
|
model.assert_not_called()
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_host_scan_timeout_propagates_without_authoritative_verdict(monkeypatch):
|
|
from deerflow.skills.mutations.scanner import CandidateScanner
|
|
|
|
monkeypatch.setattr("deerflow.skills.mutations.scanner.CHECK_TIMEOUT_SECONDS", 0)
|
|
with pytest.raises(TimeoutError):
|
|
await CandidateScanner(configuration).scan(PackageSnapshot((PackageFile("SKILL.md", CONTENT, False),)), "example")
|