From 746d7f5415b424ba9736e411b78c34e9ba6bc0d2 Mon Sep 17 00:00:00 2001 From: angleyanalbedo <100198247+angleyanalbedo@users.noreply.github.com> Date: Tue, 10 Mar 2026 15:10:09 +0800 Subject: [PATCH 1/3] feat(tools): enhance ExecTool with enable flag and custom deny_patterns - Add `enable` flag to `ExecToolConfig` to conditionally register the tool. - Add `deny_patterns` to allow users to override the default command blacklist. - Remove `allow_patterns` (whitelist) to maintain tool flexibility. - Fix initialization logic to properly handle empty list (`[]`), allowing users to completely clear the default blacklist. --- nanobot/agent/loop.py | 14 ++++++++------ nanobot/config/schema.py | 3 ++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index ca9a06e..bf40214 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -117,12 +117,14 @@ class AgentLoop: allowed_dir = self.workspace if self.restrict_to_workspace else None for cls in (ReadFileTool, WriteFileTool, EditFileTool, ListDirTool): self.tools.register(cls(workspace=self.workspace, allowed_dir=allowed_dir)) - self.tools.register(ExecTool( - working_dir=str(self.workspace), - timeout=self.exec_config.timeout, - restrict_to_workspace=self.restrict_to_workspace, - path_append=self.exec_config.path_append, - )) + if self.exec_config.enable: + self.tools.register(ExecTool( + working_dir=str(self.workspace), + timeout=self.exec_config.timeout, + restrict_to_workspace=self.restrict_to_workspace, + path_append=self.exec_config.path_append, + deny_patterns=self.exec_config.deny_patterns, + )) self.tools.register(WebSearchTool(api_key=self.brave_api_key, proxy=self.web_proxy)) self.tools.register(WebFetchTool(proxy=self.web_proxy)) self.tools.register(MessageTool(send_callback=self.bus.publish_outbound)) diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 8cfcad6..a1d6ed4 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -305,9 +305,10 @@ class WebToolsConfig(Base): class ExecToolConfig(Base): """Shell exec tool configuration.""" + enable: bool = True timeout: int = 60 path_append: str = "" - + deny_patterns: list[str] | None = None class MCPServerConfig(Base): """MCP server connection configuration (stdio or HTTP).""" From dc1aeeaf8bb119a8cbddca37ddac592d9ad4fc84 Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Fri, 20 Mar 2026 17:24:40 +0000 Subject: [PATCH 2/3] docs: document exec tool enable and denyPatterns Made-with: Cursor --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 8ac23a0..fec7963 100644 --- a/README.md +++ b/README.md @@ -1163,7 +1163,9 @@ MCP tools are automatically discovered and registered on startup. The LLM can us | 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.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.denyPatterns` | `null` | Optional regex blacklist for shell commands. Set this to override the default dangerous-command patterns used by `exec`. | | `channels.*.allowFrom` | `[]` (deny all) | Whitelist of user IDs. Empty denies all; use `["*"]` to allow everyone. | From 1c39a4d311ee4a9898a798ab82b4ab3aad990e9f Mon Sep 17 00:00:00 2001 From: Xubin Ren Date: Fri, 20 Mar 2026 17:46:08 +0000 Subject: [PATCH 3/3] refactor(tools): keep exec enable without configurable deny patterns Made-with: Cursor --- README.md | 1 - nanobot/agent/loop.py | 1 - nanobot/agent/tools/shell.py | 2 +- nanobot/config/schema.py | 1 - tests/test_task_cancel.py | 10 ---------- tests/test_tool_validation.py | 6 ------ 6 files changed, 1 insertion(+), 20 deletions(-) diff --git a/README.md b/README.md index fec7963..9f23e15 100644 --- a/README.md +++ b/README.md @@ -1165,7 +1165,6 @@ MCP tools are automatically discovered and registered on startup. The LLM can us | `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.denyPatterns` | `null` | Optional regex blacklist for shell commands. Set this to override the default dangerous-command patterns used by `exec`. | | `channels.*.allowFrom` | `[]` (deny all) | Whitelist of user IDs. Empty denies all; use `["*"]` to allow everyone. | diff --git a/nanobot/agent/loop.py b/nanobot/agent/loop.py index 574a150..be820ef 100644 --- a/nanobot/agent/loop.py +++ b/nanobot/agent/loop.py @@ -126,7 +126,6 @@ class AgentLoop: timeout=self.exec_config.timeout, restrict_to_workspace=self.restrict_to_workspace, path_append=self.exec_config.path_append, - deny_patterns=self.exec_config.deny_patterns, )) self.tools.register(WebSearchTool(config=self.web_search_config, proxy=self.web_proxy)) self.tools.register(WebFetchTool(proxy=self.web_proxy)) diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index a59a878..4b10c83 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -23,7 +23,7 @@ class ExecTool(Tool): ): self.timeout = timeout self.working_dir = working_dir - self.deny_patterns = deny_patterns if deny_patterns is not None else [ + self.deny_patterns = deny_patterns or [ r"\brm\s+-[rf]{1,2}\b", # rm -r, rm -rf, rm -fr r"\bdel\s+/[fq]\b", # del /f, del /q r"\brmdir\s+/s\b", # rmdir /s diff --git a/nanobot/config/schema.py b/nanobot/config/schema.py index 7f119b4..78cba1d 100644 --- a/nanobot/config/schema.py +++ b/nanobot/config/schema.py @@ -121,7 +121,6 @@ class ExecToolConfig(Base): enable: bool = True timeout: int = 60 path_append: str = "" - deny_patterns: list[str] | None = None class MCPServerConfig(Base): """MCP server connection configuration (stdio or HTTP).""" diff --git a/tests/test_task_cancel.py b/tests/test_task_cancel.py index d323585..5bc2ea9 100644 --- a/tests/test_task_cancel.py +++ b/tests/test_task_cancel.py @@ -97,16 +97,6 @@ class TestDispatch: assert loop.tools.get("exec") is None - def test_exec_tool_receives_custom_deny_patterns(self): - from nanobot.agent.tools.shell import ExecTool - from nanobot.config.schema import ExecToolConfig - - loop, _bus = _make_loop(exec_config=ExecToolConfig(deny_patterns=[r"\becho\b"])) - tool = loop.tools.get("exec") - - assert isinstance(tool, ExecTool) - assert tool.deny_patterns == [r"\becho\b"] - @pytest.mark.asyncio async def test_dispatch_processes_and_publishes(self): from nanobot.bus.events import InboundMessage, OutboundMessage diff --git a/tests/test_tool_validation.py b/tests/test_tool_validation.py index e4f0063..e817f37 100644 --- a/tests/test_tool_validation.py +++ b/tests/test_tool_validation.py @@ -134,12 +134,6 @@ def test_exec_guard_blocks_quoted_home_path_outside_workspace(tmp_path) -> None: assert error == "Error: Command blocked by safety guard (path outside working dir)" -def test_exec_empty_deny_patterns_override_defaults() -> None: - tool = ExecTool(deny_patterns=[]) - error = tool._guard_command("rm -rf /tmp/demo", "/tmp") - assert error is None - - # --- cast_params tests ---