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.
This commit is contained in:
chengyongru
2026-06-15 02:55:18 +08:00
committed by Xubin Ren
parent fee21332d3
commit 44ce220af6
4 changed files with 64 additions and 7 deletions
+5 -4
View File
@@ -16,16 +16,16 @@ from nanobot.agent.tools.context import ToolContext
from nanobot.agent.tools.file_state import FileStates from nanobot.agent.tools.file_state import FileStates
from nanobot.agent.tools.loader import ToolLoader from nanobot.agent.tools.loader import ToolLoader
from nanobot.agent.tools.registry import ToolRegistry 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 ( from nanobot.security.workspace_access import (
WorkspaceScope, WorkspaceScope,
bind_workspace_scope, bind_workspace_scope,
reset_workspace_scope, reset_workspace_scope,
workspace_sandbox_status, 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 from nanobot.utils.prompt_templates import render_template
@@ -118,6 +118,7 @@ class SubagentManager:
return ToolsConfig( return ToolsConfig(
exec=self.tools_config.exec, exec=self.tools_config.exec,
web=self.tools_config.web, web=self.tools_config.web,
file=self.tools_config.file,
restrict_to_workspace=self.restrict_to_workspace, restrict_to_workspace=self.restrict_to_workspace,
) )
+2 -2
View File
@@ -10,14 +10,14 @@ from typing import Any
from nanobot.agent.tools.base import Tool, tool_parameters 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.file_state import FileStates, _hash_file, current_file_states
from nanobot.agent.tools.path_utils import resolve_workspace_path 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 ( from nanobot.agent.tools.schema import (
BooleanSchema, BooleanSchema,
IntegerSchema, IntegerSchema,
StringSchema, StringSchema,
tool_parameters_schema, 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 from nanobot.utils.helpers import build_image_content_blocks, detect_image_mime
+28
View File
@@ -6,7 +6,9 @@ from unittest.mock import MagicMock
import pytest import pytest
from nanobot.agent.subagent import SubagentManager from nanobot.agent.subagent import SubagentManager
from nanobot.agent.tools.filesystem import FileToolsConfig
from nanobot.bus.queue import MessageBus from nanobot.bus.queue import MessageBus
from nanobot.config.schema import ToolsConfig
from nanobot.providers.base import LLMProvider 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") second_result = await second_read.execute(path="note.txt")
assert second_result.startswith("1| hello") assert second_result.startswith("1| hello")
assert "File unchanged" not in second_result 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)
+29 -1
View File
@@ -1,7 +1,21 @@
from types import SimpleNamespace 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.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(): def test_file_tools_enabled_by_default():
@@ -14,3 +28,17 @@ def test_file_tool_gate_follows_flag():
cfg.file.enable = False cfg.file.enable = False
assert ReadFileTool.enabled(SimpleNamespace(config=cfg)) is False assert ReadFileTool.enabled(SimpleNamespace(config=cfg)) is False
assert ReadFileTool.enabled(SimpleNamespace(config=ToolsConfig())) is True 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)