diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index 279e2e77..849dcf6e 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -541,8 +541,9 @@ class ExecTool(Tool): def _build_env(self) -> dict[str, str]: """Build a minimal environment for subprocess execution. - On Unix, only HOME/LANG/TERM are passed; ``bash -l`` sources the - user's profile which sets PATH and other essentials. + On Unix, only HOME/LANG/TERM are passed by default. If callers request + ``login=True``, bash/zsh may source the user's profile and add PATH or + other variables. On Windows, ``cmd.exe`` has no login-profile mechanism, so a curated set of system variables (including PATH) is forwarded. API keys and diff --git a/tests/tools/test_exec_platform.py b/tests/tools/test_exec_platform.py index 039888ff..0a5337f6 100644 --- a/tests/tools/test_exec_platform.py +++ b/tests/tools/test_exec_platform.py @@ -400,6 +400,29 @@ class TestExecuteEndToEnd: assert "hello world" in result assert "Exit code: 0" in result + @pytest.mark.asyncio + async def test_execute_defaults_to_non_login_shell(self): + """The public execute path must not silently request a login shell.""" + mock_proc = AsyncMock() + mock_proc.communicate.return_value = (b"ok\n", b"") + mock_proc.returncode = 0 + captured_login = [] + + async def capture_spawn(cmd, cwd, env, shell_program=None, login=None, *, stdin=None): + captured_login.append(login) + return mock_proc + + with ( + patch("nanobot.agent.tools.shell._IS_WINDOWS", False), + patch.object(ExecTool, "_spawn", side_effect=capture_spawn), + patch.object(ExecTool, "_guard_command", return_value=None), + ): + tool = ExecTool() + await tool.execute(command="echo ok") + await tool.execute(command="echo ok", login=True) + + assert captured_login == [False, True] + # --------------------------------------------------------------------------- # _extract_absolute_paths - UNC path support