From d78ed5c8f2673da21f1855c74341d7bda15776aa Mon Sep 17 00:00:00 2001 From: DanielWalnut <45447813+hetaoBackend@users.noreply.github.com> Date: Fri, 24 Apr 2026 21:24:42 +0800 Subject: [PATCH] fix: inherit subagent skill allowlists (#2514) --- .../deerflow/agents/lead_agent/agent.py | 1 + .../deerflow/tools/builtins/task_tool.py | 21 ++++- backend/tests/test_task_tool_core_logic.py | 84 +++++++++++++++++++ 3 files changed, 103 insertions(+), 3 deletions(-) diff --git a/backend/packages/harness/deerflow/agents/lead_agent/agent.py b/backend/packages/harness/deerflow/agents/lead_agent/agent.py index f17aab6ce..1d1efe5b0 100644 --- a/backend/packages/harness/deerflow/agents/lead_agent/agent.py +++ b/backend/packages/harness/deerflow/agents/lead_agent/agent.py @@ -350,6 +350,7 @@ def make_lead_agent(config: RunnableConfig): "is_plan_mode": is_plan_mode, "subagent_enabled": subagent_enabled, "tool_groups": agent_config.tool_groups if agent_config else None, + "available_skills": ["bootstrap"] if is_bootstrap else (agent_config.skills if agent_config and agent_config.skills is not None else None), } ) diff --git a/backend/packages/harness/deerflow/tools/builtins/task_tool.py b/backend/packages/harness/deerflow/tools/builtins/task_tool.py index fbe41ded7..59613272c 100644 --- a/backend/packages/harness/deerflow/tools/builtins/task_tool.py +++ b/backend/packages/harness/deerflow/tools/builtins/task_tool.py @@ -18,6 +18,17 @@ from deerflow.subagents.executor import SubagentStatus, cleanup_background_task, logger = logging.getLogger(__name__) +def _merge_skill_allowlists(parent: list[str] | None, child: list[str] | None) -> list[str] | None: + """Return the effective subagent skill allowlist under the parent policy.""" + if parent is None: + return child + if child is None: + return list(parent) + + parent_set = set(parent) + return [skill for skill in child if skill in parent_set] + + @tool("task", parse_docstring=True) async def task_tool( runtime: ToolRuntime[ContextT, ThreadState], @@ -83,9 +94,6 @@ async def task_tool( if max_turns is not None: overrides["max_turns"] = max_turns - if overrides: - config = replace(config, **overrides) - # Extract parent context from runtime sandbox_state = None thread_data = None @@ -108,6 +116,13 @@ async def task_tool( # Get or generate trace_id for distributed tracing trace_id = metadata.get("trace_id") or str(uuid.uuid4())[:8] + parent_available_skills = metadata.get("available_skills") + if parent_available_skills is not None: + overrides["skills"] = _merge_skill_allowlists(list(parent_available_skills), config.skills) + + if overrides: + config = replace(config, **overrides) + # Get available tools (excluding task tool to prevent nesting) # Lazy import to avoid circular dependency from deerflow.tools import get_available_tools diff --git a/backend/tests/test_task_tool_core_logic.py b/backend/tests/test_task_tool_core_logic.py index b39f09dad..1ae008df2 100644 --- a/backend/tests/test_task_tool_core_logic.py +++ b/backend/tests/test_task_tool_core_logic.py @@ -223,6 +223,90 @@ def test_task_tool_propagates_tool_groups_to_subagent(monkeypatch): get_available_tools.assert_called_once_with(model_name="ark-model", groups=parent_tool_groups, subagent_enabled=False) +def test_task_tool_inherits_parent_skill_allowlist_for_default_subagent(monkeypatch): + config = _make_subagent_config() + runtime = _make_runtime() + runtime.config["metadata"]["available_skills"] = ["safe-skill"] + events = [] + captured = {} + + class DummyExecutor: + def __init__(self, **kwargs): + captured["config"] = kwargs["config"] + + def execute_async(self, prompt, task_id=None): + return task_id or "generated-task-id" + + monkeypatch.setattr(task_tool_module, "SubagentStatus", FakeSubagentStatus) + monkeypatch.setattr(task_tool_module, "SubagentExecutor", DummyExecutor) + monkeypatch.setattr(task_tool_module, "get_subagent_config", lambda _: config) + monkeypatch.setattr( + task_tool_module, + "get_background_task_result", + lambda _: _make_result(FakeSubagentStatus.COMPLETED, result="done"), + ) + monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append) + monkeypatch.setattr(task_tool_module.asyncio, "sleep", _no_sleep) + monkeypatch.setattr("deerflow.tools.get_available_tools", MagicMock(return_value=[])) + + output = _run_task_tool( + runtime=runtime, + description="执行任务", + prompt="use skills", + subagent_type="general-purpose", + tool_call_id="tc-skills", + ) + + assert output == "Task Succeeded. Result: done" + assert captured["config"].skills == ["safe-skill"] + + +def test_task_tool_intersects_parent_and_subagent_skill_allowlists(monkeypatch): + config = _make_subagent_config() + config = SubagentConfig( + name=config.name, + description=config.description, + system_prompt=config.system_prompt, + max_turns=config.max_turns, + timeout_seconds=config.timeout_seconds, + skills=["safe-skill", "other-skill"], + ) + runtime = _make_runtime() + runtime.config["metadata"]["available_skills"] = ["safe-skill"] + events = [] + captured = {} + + class DummyExecutor: + def __init__(self, **kwargs): + captured["config"] = kwargs["config"] + + def execute_async(self, prompt, task_id=None): + return task_id or "generated-task-id" + + monkeypatch.setattr(task_tool_module, "SubagentStatus", FakeSubagentStatus) + monkeypatch.setattr(task_tool_module, "SubagentExecutor", DummyExecutor) + monkeypatch.setattr(task_tool_module, "get_subagent_config", lambda _: config) + monkeypatch.setattr( + task_tool_module, + "get_background_task_result", + lambda _: _make_result(FakeSubagentStatus.COMPLETED, result="done"), + ) + monkeypatch.setattr(task_tool_module, "get_stream_writer", lambda: events.append) + monkeypatch.setattr(task_tool_module.asyncio, "sleep", _no_sleep) + monkeypatch.setattr("deerflow.tools.get_available_tools", MagicMock(return_value=[])) + + output = _run_task_tool( + runtime=runtime, + description="执行任务", + prompt="use skills", + subagent_type="general-purpose", + tool_call_id="tc-skills-intersection", + ) + + assert output == "Task Succeeded. Result: done" + assert captured["config"].skills == ["safe-skill"] + + def test_task_tool_no_tool_groups_passes_none(monkeypatch): """Verify that when metadata has no tool_groups, groups=None is passed (backward compat).""" config = _make_subagent_config()