diff --git a/nanobot/agent/tools/shell.py b/nanobot/agent/tools/shell.py index 08c58499..9484c73f 100644 --- a/nanobot/agent/tools/shell.py +++ b/nanobot/agent/tools/shell.py @@ -135,7 +135,11 @@ class ExecTool(Tool): env = self._build_env() if self.path_append: - env["PATH"] = env.get("PATH", "") + os.pathsep + self.path_append + if _IS_WINDOWS: + env["PATH"] = env.get("PATH", "") + os.pathsep + self.path_append + else: + env["NANOBOT_PATH_APPEND"] = self.path_append + command = f'export PATH="$PATH{os.pathsep}$NANOBOT_PATH_APPEND"; {command}' try: process = await self._spawn(command, cwd, env) @@ -255,7 +259,6 @@ class ExecTool(Tool): home = os.environ.get("HOME", "/tmp") env = { "HOME": home, - "PATH": os.environ.get ("PATH", "/usr/bin:/bin"), "LANG": os.environ.get("LANG", "C.UTF-8"), "TERM": os.environ.get("TERM", "dumb"), } @@ -296,8 +299,8 @@ class ExecTool(Tool): continue media_path = get_media_dir().resolve() - if (p.is_absolute() - and cwd_path not in p.parents + if (p.is_absolute() + and cwd_path not in p.parents and p != cwd_path and media_path not in p.parents and p != media_path diff --git a/tests/tools/test_exec_platform.py b/tests/tools/test_exec_platform.py index 47f872eb..b3d7f4c1 100644 --- a/tests/tools/test_exec_platform.py +++ b/tests/tools/test_exec_platform.py @@ -27,7 +27,7 @@ class TestBuildEnvUnix: def test_expected_keys(self): with patch("nanobot.agent.tools.shell._IS_WINDOWS", False): env = ExecTool()._build_env() - expected = {"HOME", "LANG", "PATH","TERM"} + expected = {"HOME", "LANG", "TERM"} assert expected <= set(env) if sys.platform != "win32": assert set(env) == expected @@ -148,33 +148,33 @@ class TestSpawnWindows: class TestPathAppendPlatform: @pytest.mark.asyncio - async def test_unix_injects_export (self): - """On Unix, path_append is passed via the env dict, not shell string.""" - mock_proc = AsyncMock () + async def test_unix_uses_env_var_in_fixed_export(self): + """On Unix, path_append must not be interpolated into shell source.""" + mock_proc = AsyncMock() mock_proc.communicate.return_value = (b"ok", b"") mock_proc.returncode = 0 captured_cmd = None captured_env = {} - async def capture_spawn (cmd, cwd, env): + async def capture_spawn(cmd, cwd, env): nonlocal captured_cmd captured_cmd = cmd - captured_env.update (env) + captured_env.update(env) return mock_proc with ( - patch ("nanobot.agent.tools.shell._IS_WINDOWS", False), - patch ("nanobot.agent.tools.shell.os.pathsep", ":"), - patch.object (ExecTool, "_spawn", side_effect=capture_spawn), - patch.object (ExecTool, "_guard_command", return_value=None), + patch("nanobot.agent.tools.shell._IS_WINDOWS", False), + patch("nanobot.agent.tools.shell.os.pathsep", ":"), + patch.object(ExecTool, "_spawn", side_effect=capture_spawn), + patch.object(ExecTool, "_guard_command", return_value=None), ): - tool = ExecTool (path_append="/opt/bin") - await tool.execute (command="ls") + tool = ExecTool(path_append="/opt/bin; echo INJECTED") + await tool.execute(command="ls") - # path_append must be in the env dict, NOT injected into the command - assert captured_cmd == "ls" - assert captured_env["PATH"].endswith (":/opt/bin") + assert captured_cmd == 'export PATH="$PATH:$NANOBOT_PATH_APPEND"; ls' + assert captured_env["NANOBOT_PATH_APPEND"] == "/opt/bin; echo INJECTED" + assert "INJECTED" not in captured_cmd @pytest.mark.asyncio async def test_windows_modifies_env(self): @@ -191,7 +191,7 @@ class TestPathAppendPlatform: with ( patch("nanobot.agent.tools.shell._IS_WINDOWS", True), - patch ("nanobot.agent.tools.shell.os.pathsep", ";"), + patch("nanobot.agent.tools.shell.os.pathsep", ";"), patch.object(ExecTool, "_spawn", side_effect=capture_spawn), patch.object(ExecTool, "_guard_command", return_value=None), ):