Merge PR #1824: feat(tools): enhance ExecTool with enable flag
feat(tools): enhance ExecTool with enable flag
This commit is contained in:
@@ -1163,6 +1163,7 @@ MCP tools are automatically discovered and registered on startup. The LLM can us
|
|||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
|--------|---------|-------------|
|
|--------|---------|-------------|
|
||||||
| `tools.restrictToWorkspace` | `false` | When `true`, restricts **all** agent tools (shell, file read/write/edit, list) to the workspace directory. Prevents path traversal and out-of-scope access. |
|
| `tools.restrictToWorkspace` | `false` | When `true`, restricts **all** agent tools (shell, file read/write/edit, list) to the workspace directory. Prevents path traversal and out-of-scope access. |
|
||||||
|
| `tools.exec.enable` | `true` | When `false`, the shell `exec` tool is not registered at all. Use this to completely disable shell command execution. |
|
||||||
| `tools.exec.pathAppend` | `""` | Extra directories to append to `PATH` when running shell commands (e.g. `/usr/sbin` for `ufw`). |
|
| `tools.exec.pathAppend` | `""` | Extra directories to append to `PATH` when running shell commands (e.g. `/usr/sbin` for `ufw`). |
|
||||||
| `channels.*.allowFrom` | `[]` (deny all) | Whitelist of user IDs. Empty denies all; use `["*"]` to allow everyone. |
|
| `channels.*.allowFrom` | `[]` (deny all) | Whitelist of user IDs. Empty denies all; use `["*"]` to allow everyone. |
|
||||||
|
|
||||||
|
|||||||
@@ -120,6 +120,7 @@ class AgentLoop:
|
|||||||
self.tools.register(ReadFileTool(workspace=self.workspace, allowed_dir=allowed_dir, extra_allowed_dirs=extra_read))
|
self.tools.register(ReadFileTool(workspace=self.workspace, allowed_dir=allowed_dir, extra_allowed_dirs=extra_read))
|
||||||
for cls in (WriteFileTool, EditFileTool, ListDirTool):
|
for cls in (WriteFileTool, EditFileTool, ListDirTool):
|
||||||
self.tools.register(cls(workspace=self.workspace, allowed_dir=allowed_dir))
|
self.tools.register(cls(workspace=self.workspace, allowed_dir=allowed_dir))
|
||||||
|
if self.exec_config.enable:
|
||||||
self.tools.register(ExecTool(
|
self.tools.register(ExecTool(
|
||||||
working_dir=str(self.workspace),
|
working_dir=str(self.workspace),
|
||||||
timeout=self.exec_config.timeout,
|
timeout=self.exec_config.timeout,
|
||||||
|
|||||||
@@ -118,10 +118,10 @@ class WebToolsConfig(Base):
|
|||||||
class ExecToolConfig(Base):
|
class ExecToolConfig(Base):
|
||||||
"""Shell exec tool configuration."""
|
"""Shell exec tool configuration."""
|
||||||
|
|
||||||
|
enable: bool = True
|
||||||
timeout: int = 60
|
timeout: int = 60
|
||||||
path_append: str = ""
|
path_append: str = ""
|
||||||
|
|
||||||
|
|
||||||
class MCPServerConfig(Base):
|
class MCPServerConfig(Base):
|
||||||
"""MCP server connection configuration (stdio or HTTP)."""
|
"""MCP server connection configuration (stdio or HTTP)."""
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ from unittest.mock import AsyncMock, MagicMock, patch
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
|
||||||
def _make_loop():
|
def _make_loop(*, exec_config=None):
|
||||||
"""Create a minimal AgentLoop with mocked dependencies."""
|
"""Create a minimal AgentLoop with mocked dependencies."""
|
||||||
from nanobot.agent.loop import AgentLoop
|
from nanobot.agent.loop import AgentLoop
|
||||||
from nanobot.bus.queue import MessageBus
|
from nanobot.bus.queue import MessageBus
|
||||||
@@ -23,7 +23,7 @@ def _make_loop():
|
|||||||
patch("nanobot.agent.loop.SessionManager"), \
|
patch("nanobot.agent.loop.SessionManager"), \
|
||||||
patch("nanobot.agent.loop.SubagentManager") as MockSubMgr:
|
patch("nanobot.agent.loop.SubagentManager") as MockSubMgr:
|
||||||
MockSubMgr.return_value.cancel_by_session = AsyncMock(return_value=0)
|
MockSubMgr.return_value.cancel_by_session = AsyncMock(return_value=0)
|
||||||
loop = AgentLoop(bus=bus, provider=provider, workspace=workspace)
|
loop = AgentLoop(bus=bus, provider=provider, workspace=workspace, exec_config=exec_config)
|
||||||
return loop, bus
|
return loop, bus
|
||||||
|
|
||||||
|
|
||||||
@@ -90,6 +90,13 @@ class TestHandleStop:
|
|||||||
|
|
||||||
|
|
||||||
class TestDispatch:
|
class TestDispatch:
|
||||||
|
def test_exec_tool_not_registered_when_disabled(self):
|
||||||
|
from nanobot.config.schema import ExecToolConfig
|
||||||
|
|
||||||
|
loop, _bus = _make_loop(exec_config=ExecToolConfig(enable=False))
|
||||||
|
|
||||||
|
assert loop.tools.get("exec") is None
|
||||||
|
|
||||||
@pytest.mark.asyncio
|
@pytest.mark.asyncio
|
||||||
async def test_dispatch_processes_and_publishes(self):
|
async def test_dispatch_processes_and_publishes(self):
|
||||||
from nanobot.bus.events import InboundMessage, OutboundMessage
|
from nanobot.bus.events import InboundMessage, OutboundMessage
|
||||||
|
|||||||
Reference in New Issue
Block a user