From 09d3bd76c9c1fa3c64344bd44e6311097f156355 Mon Sep 17 00:00:00 2001 From: chengyongru Date: Mon, 24 Aug 2026 11:30:48 +0800 Subject: [PATCH] fix(exec): disable command guard in full access --- nanobot/agent/tools/shell.py | 20 +++++++------ tests/tools/test_exec_security.py | 40 ++++++++++++++++++++++++-- tests/tools/test_exec_session_tools.py | 1 + 3 files changed, 50 insertions(+), 11 deletions(-) diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index 19f1d9f9a..54881fe23 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -470,14 +470,18 @@ class ExecTool(Tool): + _WORKSPACE_BOUNDARY_NOTE ) - guard_error = self._guard_command( - command, - cwd, - restrict_to_workspace=access.restrict_to_workspace, - workspace_root=workspace_root, - ) - if guard_error: - return guard_error + # Full access is an explicit trust decision. Keep the application-level + # command guard aligned with the selected access mode instead of + # continuing to block commands after workspace restriction is disabled. + if access.restrict_to_workspace: + guard_error = self._guard_command( + command, + cwd, + restrict_to_workspace=True, + workspace_root=workspace_root, + ) + if guard_error: + return guard_error if self.sandbox: if _IS_WINDOWS: diff --git a/tests/tools/test_exec_security.py b/tests/tools/test_exec_security.py index 5c0c4c49d..6a6b1337c 100644 --- a/tests/tools/test_exec_security.py +++ b/tests/tools/test_exec_security.py @@ -30,7 +30,7 @@ def _fake_resolve_public(hostname, port, family=0, type_=0): @pytest.mark.asyncio async def test_exec_blocks_curl_metadata(): - tool = ExecTool() + tool = ExecTool(restrict_to_workspace=True) with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve_private): result = await tool.execute( command='curl -s -H "Metadata-Flavor: Google" http://169.254.169.254/computeMetadata/v1/' @@ -41,7 +41,7 @@ async def test_exec_blocks_curl_metadata(): @pytest.mark.asyncio async def test_exec_blocks_wget_localhost(): - tool = ExecTool() + tool = ExecTool(restrict_to_workspace=True) with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve_localhost): result = await tool.execute(command="wget http://localhost:8080/secret -O /tmp/out") assert "Error" in result @@ -111,6 +111,40 @@ def test_exec_full_workspace_scope_still_blocks_metadata(tmp_path): assert "internal/private" in error +@pytest.mark.parametrize( + "command", + [ + "echo blocked", + "echo http://169.254.169.254/latest/meta-data/", + ], +) +async def test_exec_full_access_skips_command_guard(tmp_path, command): + tool = ExecTool( + working_dir=str(tmp_path), + restrict_to_workspace=False, + deny_patterns=[r"echo\s+blocked"], + ) + result = await tool.execute(command=command) + + assert "Exit code: 0" in result + assert "Command blocked" not in result + + +async def test_exec_full_workspace_scope_skips_command_guard(tmp_path): + tool = ExecTool(working_dir=str(tmp_path), restrict_to_workspace=True) + scope = build_workspace_scope(tmp_path, "full", source_channel="websocket") + token = bind_workspace_scope(scope) + try: + result = await tool.execute( + command="echo http://169.254.169.254/latest/meta-data/", + ) + finally: + reset_workspace_scope(token) + + assert "Exit code: 0" in result + assert "Command blocked" not in result + + @pytest.mark.asyncio async def test_exec_allows_normal_commands(): tool = ExecTool(timeout=5) @@ -131,7 +165,7 @@ async def test_exec_allows_curl_to_public_url(): @pytest.mark.asyncio async def test_exec_blocks_chained_internal_url(): """Internal URLs buried in chained commands should still be caught.""" - tool = ExecTool() + tool = ExecTool(restrict_to_workspace=True) with patch("nanobot.security.network.socket.getaddrinfo", _fake_resolve_private): result = await tool.execute( command="echo start && curl http://169.254.169.254/latest/meta-data/ && echo done" diff --git a/tests/tools/test_exec_session_tools.py b/tests/tools/test_exec_session_tools.py index ed148e885..4b6ce83eb 100644 --- a/tests/tools/test_exec_session_tools.py +++ b/tests/tools/test_exec_session_tools.py @@ -525,6 +525,7 @@ def test_exec_session_mode_reuses_exec_safety_guard(tmp_path): tool = ExecTool( working_dir=str(tmp_path), deny_patterns=[r"echo\s+blocked"], + restrict_to_workspace=True, session_manager=manager, )