From 44ce220af6ef6e8be3d0dde9b181d90832cf2dfa Mon Sep 17 00:00:00 2001 From: chengyongru <2755839590@qq.com> Date: Sat, 13 Jun 2026 20:42:14 +0800 Subject: [PATCH] fix: preserve file tool toggle for subagents Maintainer edit: subagents rebuilt their scoped ToolsConfig without carrying tools.file, which re-enabled built-in file tools after the parent agent disabled them. Preserve the file config and add loader/subagent coverage for the disabled path. --- nanobot/agent/subagent.py | 9 +++++---- nanobot/agent/tools/filesystem.py | 4 ++-- tests/agent/test_subagent.py | 28 ++++++++++++++++++++++++++++ tests/test_file_tool_toggle.py | 30 +++++++++++++++++++++++++++++- 4 files changed, 64 insertions(+), 7 deletions(-) diff --git a/nanobot/agent/subagent.py b/nanobot/agent/subagent.py index 88c22e61..d93f8419 100644 --- a/nanobot/agent/subagent.py +++ b/nanobot/agent/subagent.py @@ -16,16 +16,16 @@ from nanobot.agent.tools.context import ToolContext from nanobot.agent.tools.file_state import FileStates from nanobot.agent.tools.loader import ToolLoader from nanobot.agent.tools.registry import ToolRegistry +from nanobot.bus.events import InboundMessage +from nanobot.bus.queue import MessageBus +from nanobot.config.schema import AgentDefaults, ToolsConfig +from nanobot.providers.base import LLMProvider from nanobot.security.workspace_access import ( WorkspaceScope, bind_workspace_scope, reset_workspace_scope, workspace_sandbox_status, ) -from nanobot.bus.events import InboundMessage -from nanobot.bus.queue import MessageBus -from nanobot.config.schema import AgentDefaults, ToolsConfig -from nanobot.providers.base import LLMProvider from nanobot.utils.prompt_templates import render_template @@ -118,6 +118,7 @@ class SubagentManager: return ToolsConfig( exec=self.tools_config.exec, web=self.tools_config.web, + file=self.tools_config.file, restrict_to_workspace=self.restrict_to_workspace, ) diff --git a/nanobot/agent/tools/filesystem.py b/nanobot/agent/tools/filesystem.py index 92e1e90f..23549ef9 100644 --- a/nanobot/agent/tools/filesystem.py +++ b/nanobot/agent/tools/filesystem.py @@ -10,14 +10,14 @@ from typing import Any from nanobot.agent.tools.base import Tool, tool_parameters from nanobot.agent.tools.file_state import FileStates, _hash_file, current_file_states from nanobot.agent.tools.path_utils import resolve_workspace_path -from nanobot.config.schema import Base -from nanobot.security.workspace_access import current_tool_workspace from nanobot.agent.tools.schema import ( BooleanSchema, IntegerSchema, StringSchema, tool_parameters_schema, ) +from nanobot.config.schema import Base +from nanobot.security.workspace_access import current_tool_workspace from nanobot.utils.helpers import build_image_content_blocks, detect_image_mime diff --git a/tests/agent/test_subagent.py b/tests/agent/test_subagent.py index 5bdfc18d..32b5aa6a 100644 --- a/tests/agent/test_subagent.py +++ b/tests/agent/test_subagent.py @@ -6,7 +6,9 @@ from unittest.mock import MagicMock import pytest from nanobot.agent.subagent import SubagentManager +from nanobot.agent.tools.filesystem import FileToolsConfig from nanobot.bus.queue import MessageBus +from nanobot.config.schema import ToolsConfig from nanobot.providers.base import LLMProvider @@ -51,3 +53,29 @@ async def test_subagent_build_tools_isolates_file_read_state(tmp_path): second_result = await second_read.execute(path="note.txt") assert second_result.startswith("1| hello") assert "File unchanged" not in second_result + + +def test_subagent_respects_file_tool_toggle(tmp_path): + provider = MagicMock(spec=LLMProvider) + provider.get_default_model.return_value = "test" + sm = SubagentManager( + provider=provider, + workspace=tmp_path, + bus=MessageBus(), + model="test", + max_tool_result_chars=16_000, + tools_config=ToolsConfig(file=FileToolsConfig(enable=False)), + ) + + tools = sm._build_tools() + + file_tools = { + "apply_patch", + "edit_file", + "find_files", + "grep", + "list_dir", + "read_file", + "write_file", + } + assert file_tools.isdisjoint(tools.tool_names) diff --git a/tests/test_file_tool_toggle.py b/tests/test_file_tool_toggle.py index a3d2ed5e..3df2d00d 100644 --- a/tests/test_file_tool_toggle.py +++ b/tests/test_file_tool_toggle.py @@ -1,7 +1,21 @@ from types import SimpleNamespace -from nanobot.config.schema import Config, ToolsConfig +from nanobot.agent.tools.context import ToolContext +from nanobot.agent.tools.file_state import FileStates from nanobot.agent.tools.filesystem import FileToolsConfig, ReadFileTool +from nanobot.agent.tools.loader import ToolLoader +from nanobot.agent.tools.registry import ToolRegistry +from nanobot.config.schema import Config, ToolsConfig + +FILE_TOOL_NAMES = { + "apply_patch", + "edit_file", + "find_files", + "grep", + "list_dir", + "read_file", + "write_file", +} def test_file_tools_enabled_by_default(): @@ -14,3 +28,17 @@ def test_file_tool_gate_follows_flag(): cfg.file.enable = False assert ReadFileTool.enabled(SimpleNamespace(config=cfg)) is False assert ReadFileTool.enabled(SimpleNamespace(config=ToolsConfig())) is True + + +def test_file_tool_loader_skips_all_builtin_file_tools_when_disabled(tmp_path): + cfg = ToolsConfig(file=FileToolsConfig(enable=False)) + ctx = ToolContext( + config=cfg, + workspace=str(tmp_path), + file_state_store=FileStates(), + ) + registry = ToolRegistry() + + ToolLoader().load(ctx, registry) + + assert FILE_TOOL_NAMES.isdisjoint(registry.tool_names)