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:
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user