From 77040cabcd1cee41cd7dd825c466d3de94773d73 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 13:43:55 +0800 Subject: [PATCH 1/9] feat(shell): add background command execution with output redirection and timeout support --- astrbot/core/tools/computer_tools/shell.py | 58 +++++++++++++++++++++- tests/unit/test_computer_shell_tool.py | 15 ++++++ 2 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 tests/unit/test_computer_shell_tool.py diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index af933e83b1..bfde5b8fda 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -1,11 +1,15 @@ import json +import os +import uuid from dataclasses import dataclass, field +from pathlib import Path from astrbot.api import FunctionTool from astrbot.core.agent.run_context import ContextWrapper from astrbot.core.agent.tool import ToolExecResult from astrbot.core.astr_agent_context import AstrAgentContext from astrbot.core.computer.computer_client import get_booter +from astrbot.core.utils.astrbot_path import get_astrbot_system_tmp_path from ..registry import builtin_tool from .util import check_admin_permission, is_local_runtime, workspace_root @@ -15,6 +19,32 @@ } +def _quote_redirect_path(path: str, *, local_runtime: bool) -> str: + if local_runtime and os.name == "nt": + escaped_path = path.replace('"', '""') + else: + escaped_path = path.replace("\\", "\\\\").replace('"', '\\"') + return f'"{escaped_path}"' + + +def _build_background_output_path(*, local_runtime: bool) -> str: + file_name = f"astrbot_shell_stdout_{uuid.uuid4().hex[:8]}.log" + if local_runtime: + output_dir = Path(get_astrbot_system_tmp_path()) / "shell" + output_dir.mkdir(parents=True, exist_ok=True) + return str((output_dir / file_name).resolve(strict=False)) + return f"/tmp/{file_name}" + + +def _redirect_background_stdout_command( + command: str, + *, + output_path: str, + local_runtime: bool, +) -> str: + return f"{command} > {_quote_redirect_path(output_path, local_runtime=local_runtime)} 2>&1" + + @builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG) @dataclass class ExecuteShellTool(FunctionTool): @@ -28,14 +58,18 @@ class ExecuteShellTool(FunctionTool): "type": "string", "description": "The shell command to execute in the current runtime shell (for example, cmd.exe on Windows). Equal to 'cd {working_dir} && {your_command}'.", }, + "timeout": { + "type": "integer", + "description": "Optional timeout in seconds for the command execution.", + }, "background": { "type": "boolean", - "description": "Whether to run the command in the background.", + "description": "Run the command in the background. Use the file read tool to read the output later.", "default": False, }, "env": { "type": "object", - "description": "Optional environment variables to set for the file creation process.", + "description": "Optional environment variables to set.", "additionalProperties": {"type": "string"}, "default": {}, }, @@ -50,6 +84,7 @@ async def call( command: str, background: bool = False, env: dict = {}, + timeout: int | None = None, ) -> ToolExecResult: if permission_error := check_admin_permission(context, "Shell execution"): return permission_error @@ -67,12 +102,31 @@ async def call( current_workspace_root.mkdir(parents=True, exist_ok=True) cwd = str(current_workspace_root) + stdout_file: str | None = None + if background: + local_runtime = is_local_runtime(context) + stdout_file = _build_background_output_path( + local_runtime=local_runtime, + ) + command = _redirect_background_stdout_command( + command, + output_path=stdout_file, + local_runtime=local_runtime, + ) + result = await sb.shell.exec( command, cwd=cwd, background=background, env=env, + timeout=timeout or context.tool_call_timeout or 30, ) + if stdout_file: + result["stdout_file"] = stdout_file + result["stdout"] = ( + f"Command is running in the background. stdout/stderr is being " + f"written to `{stdout_file}`. Use astrbot_file_read_tool to read it." + ) return json.dumps(result, ensure_ascii=False) except Exception as e: return f"Error executing command: {str(e)}" diff --git a/tests/unit/test_computer_shell_tool.py b/tests/unit/test_computer_shell_tool.py new file mode 100644 index 0000000000..d8dfe4f8c0 --- /dev/null +++ b/tests/unit/test_computer_shell_tool.py @@ -0,0 +1,15 @@ +from astrbot.core.tools.computer_tools.shell import ( + _redirect_background_stdout_command, +) + + +def test_background_shell_command_redirects_output_to_file(): + command = _redirect_background_stdout_command( + "python -c 'print(123)'", + output_path="/tmp/astrbot shell output.log", + local_runtime=False, + ) + + assert command == ( + "python -c 'print(123)' > \"/tmp/astrbot shell output.log\" 2>&1" + ) From f1206d987bf7dff50663d0fc0c5b31975f6d8c26 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 21:59:49 +0800 Subject: [PATCH 2/9] feat(shell): update timeout parameter to be optional in shell execution methods --- astrbot/core/computer/booters/local.py | 2 +- astrbot/core/computer/booters/shipyard.py | 91 ++++++++++++++++++- astrbot/core/computer/booters/shipyard_neo.py | 14 ++- astrbot/core/computer/olayer/shell.py | 2 +- astrbot/core/tools/computer_tools/shell.py | 7 +- 5 files changed, 102 insertions(+), 14 deletions(-) diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index 44122361d6..289ed19e90 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -90,7 +90,7 @@ async def exec( command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = 30, + timeout: int | None = None, shell: bool = True, background: bool = False, ) -> dict[str, Any]: diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index bed1b06531..4adfde1daa 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -1,5 +1,6 @@ from __future__ import annotations +import shlex from typing import Any from shipyard import FileSystemComponent as ShipyardFileSystemComponent @@ -12,6 +13,91 @@ from .shipyard_search_file_util import search_files_via_shell +def _maybe_model_dump(value: Any) -> dict[str, Any]: + if isinstance(value, dict): + return value + if hasattr(value, "model_dump"): + dumped = value.model_dump() + if isinstance(dumped, dict): + return dumped + return {} + + +class ShipyardShellWrapper: + def __init__(self, _shipyard_shell: ShellComponent): + self._shell = _shipyard_shell + + async def exec( + self, + command: str, + cwd: str | None = None, + env: dict[str, str] | None = None, + timeout: int | None = None, + shell: bool = True, + background: bool = False, + ) -> dict[str, Any]: + if not shell: + return { + "stdout": "", + "stderr": "error: only shell mode is supported in shipyard booter.", + "exit_code": 2, + "success": False, + } + + run_command = command + if env: + env_prefix = " ".join( + f"{k}={shlex.quote(str(v))}" for k, v in sorted(env.items()) + ) + run_command = f"{env_prefix} {run_command}" + + if background: + run_command = ( + f"nohup sh -lc {shlex.quote(run_command)} >/dev/null 2>&1 & echo $!" + ) + + result = await self._shell.exec( + run_command, + timeout=timeout, + cwd=cwd, + ) + payload = _maybe_model_dump(result) + + stdout = payload.get("output", payload.get("stdout", "")) or "" + stderr = payload.get("error", payload.get("stderr", "")) or "" + exit_code = payload.get("exit_code") + if background: + pid: int | None = None + try: + pid = int(str(stdout).strip().splitlines()[-1]) + except Exception: + pid = None + return { + "pid": pid, + "stdout": ( + f"Command is running in the background. pid={pid}" + if pid is not None + else "Command was submitted in the background." + ), + "stderr": stderr, + "exit_code": exit_code, + "success": bool(payload.get("success", not stderr)), + "execution_id": payload.get("execution_id"), + "execution_time_ms": payload.get("execution_time_ms"), + "command": payload.get("command"), + } + + return { + "stdout": stdout, + "stderr": stderr, + "exit_code": exit_code, + "success": bool(payload.get("success", not stderr)), + "execution_id": payload.get("execution_id"), + "execution_time_ms": payload.get("execution_time_ms"), + "command": payload.get("command"), + } + + class ShipyardFileSystemWrapper: def __init__( self, _shipyard_fs: ShipyardFileSystemComponent, _shipyard_shell: ShellComponent @@ -107,7 +193,8 @@ async def boot(self, session_id: str) -> None: ) logger.info(f"Got sandbox ship: {ship.id} for session: {session_id}") self._ship = ship - self._fs = ShipyardFileSystemWrapper(self._ship.fs, self._ship.shell) + self._shell = ShipyardShellWrapper(self._ship.shell) + self._fs = ShipyardFileSystemWrapper(self._ship.fs, self._shell) async def shutdown(self) -> None: logger.info("[Computer] Shipyard booter shutdown.") @@ -122,7 +209,7 @@ def python(self) -> PythonComponent: @property def shell(self) -> ShellComponent: - return self._ship.shell + return self._shell async def upload_file(self, path: str, file_name: str) -> dict: """Upload file to sandbox""" diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index f3bb7e7b53..b141f918a2 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -96,7 +96,7 @@ async def exec( command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = 30, + timeout: int | None = None, shell: bool = True, background: bool = False, ) -> dict[str, Any]: @@ -116,11 +116,13 @@ async def exec( run_command = f"{env_prefix} {run_command}" if background: - run_command = f"nohup sh -lc {shlex.quote(run_command)} >/tmp/astrbot_bg.log 2>&1 & echo $!" + run_command = ( + f"nohup sh -lc {shlex.quote(run_command)} >/dev/null 2>&1 & echo $!" + ) result = await self._sandbox.shell.exec( run_command, - timeout=timeout or 30, + timeout=timeout, cwd=cwd, ) payload = _maybe_model_dump(result) @@ -136,7 +138,11 @@ async def exec( pid = None return { "pid": pid, - "stdout": stdout, + "stdout": ( + f"Command is running in the background. pid={pid}" + if pid is not None + else "Command was submitted in the background." + ), "stderr": stderr, "exit_code": exit_code, "success": bool(payload.get("success", not stderr)), diff --git a/astrbot/core/computer/olayer/shell.py b/astrbot/core/computer/olayer/shell.py index df2263b65a..c1c5875340 100644 --- a/astrbot/core/computer/olayer/shell.py +++ b/astrbot/core/computer/olayer/shell.py @@ -13,7 +13,7 @@ async def exec( command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = 30, + timeout: int | None = None, shell: bool = True, background: bool = False, ) -> dict[str, Any]: diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index bfde5b8fda..b307ed261f 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -58,10 +58,6 @@ class ExecuteShellTool(FunctionTool): "type": "string", "description": "The shell command to execute in the current runtime shell (for example, cmd.exe on Windows). Equal to 'cd {working_dir} && {your_command}'.", }, - "timeout": { - "type": "integer", - "description": "Optional timeout in seconds for the command execution.", - }, "background": { "type": "boolean", "description": "Run the command in the background. Use the file read tool to read the output later.", @@ -84,7 +80,6 @@ async def call( command: str, background: bool = False, env: dict = {}, - timeout: int | None = None, ) -> ToolExecResult: if permission_error := check_admin_permission(context, "Shell execution"): return permission_error @@ -119,7 +114,7 @@ async def call( cwd=cwd, background=background, env=env, - timeout=timeout or context.tool_call_timeout or 30, + timeout=None, ) if stdout_file: result["stdout_file"] = stdout_file From d7a54aed768f446da1bcfe075f67375d48cc7a65 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 22:05:47 +0800 Subject: [PATCH 3/9] feat(shell): set default timeout for shell execution to 10,000,000 milliseconds --- astrbot/core/computer/booters/shipyard.py | 2 +- astrbot/core/computer/booters/shipyard_neo.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index 4adfde1daa..79837794d6 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -58,7 +58,7 @@ async def exec( result = await self._shell.exec( run_command, - timeout=timeout, + timeout=timeout or 10_000_000, cwd=cwd, ) payload = _maybe_model_dump(result) diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index b141f918a2..4926cd742e 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -122,7 +122,7 @@ async def exec( result = await self._sandbox.shell.exec( run_command, - timeout=timeout, + timeout=timeout or 10_000_000, cwd=cwd, ) payload = _maybe_model_dump(result) From 988961a9a45bde103dfa1bc1412ce25acf259b09 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 22:08:48 +0800 Subject: [PATCH 4/9] feat(shell): set default timeout to 300s for shell execution --- astrbot/core/computer/booters/local.py | 4 ++-- astrbot/core/computer/booters/shipyard.py | 4 ++-- astrbot/core/computer/booters/shipyard_neo.py | 4 ++-- astrbot/core/computer/olayer/shell.py | 2 +- astrbot/core/tools/computer_tools/shell.py | 8 +++++++- 5 files changed, 14 insertions(+), 8 deletions(-) diff --git a/astrbot/core/computer/booters/local.py b/astrbot/core/computer/booters/local.py index 289ed19e90..a34d700ada 100644 --- a/astrbot/core/computer/booters/local.py +++ b/astrbot/core/computer/booters/local.py @@ -90,7 +90,7 @@ async def exec( command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = None, + timeout: int | None = 300, shell: bool = True, background: bool = False, ) -> dict[str, Any]: @@ -123,7 +123,7 @@ def _run() -> dict[str, Any]: shell=shell, cwd=working_dir, env=run_env, - timeout=timeout, + timeout=timeout or 300, capture_output=True, ) return { diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index 79837794d6..098511bbad 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -32,7 +32,7 @@ async def exec( command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = None, + timeout: int | None = 300, shell: bool = True, background: bool = False, ) -> dict[str, Any]: @@ -58,7 +58,7 @@ async def exec( result = await self._shell.exec( run_command, - timeout=timeout or 10_000_000, + timeout=timeout or 300, cwd=cwd, ) payload = _maybe_model_dump(result) diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index 4926cd742e..813befe44f 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -96,7 +96,7 @@ async def exec( command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = None, + timeout: int | None = 300, shell: bool = True, background: bool = False, ) -> dict[str, Any]: @@ -122,7 +122,7 @@ async def exec( result = await self._sandbox.shell.exec( run_command, - timeout=timeout or 10_000_000, + timeout=timeout or 300, cwd=cwd, ) payload = _maybe_model_dump(result) diff --git a/astrbot/core/computer/olayer/shell.py b/astrbot/core/computer/olayer/shell.py index c1c5875340..aef1fd3b6a 100644 --- a/astrbot/core/computer/olayer/shell.py +++ b/astrbot/core/computer/olayer/shell.py @@ -13,7 +13,7 @@ async def exec( command: str, cwd: str | None = None, env: dict[str, str] | None = None, - timeout: int | None = None, + timeout: int | None = 300, shell: bool = True, background: bool = False, ) -> dict[str, Any]: diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index b307ed261f..03bc420b43 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -58,6 +58,11 @@ class ExecuteShellTool(FunctionTool): "type": "string", "description": "The shell command to execute in the current runtime shell (for example, cmd.exe on Windows). Equal to 'cd {working_dir} && {your_command}'.", }, + "timeout": { + "type": "integer", + "description": "Optional timeout in seconds for the command execution.", + "default": 300, + }, "background": { "type": "boolean", "description": "Run the command in the background. Use the file read tool to read the output later.", @@ -80,6 +85,7 @@ async def call( command: str, background: bool = False, env: dict = {}, + timeout: int | None = 300, ) -> ToolExecResult: if permission_error := check_admin_permission(context, "Shell execution"): return permission_error @@ -114,7 +120,7 @@ async def call( cwd=cwd, background=background, env=env, - timeout=None, + timeout=timeout or 300, ) if stdout_file: result["stdout_file"] = stdout_file From ffa7508a8bc5561cc8aa319a6fce67298747f1f0 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 22:10:41 +0800 Subject: [PATCH 5/9] feat(shell): reorder timeout parameter in ExecuteShellTool configuration --- astrbot/core/tools/computer_tools/shell.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index 03bc420b43..11f1692c5c 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -58,16 +58,16 @@ class ExecuteShellTool(FunctionTool): "type": "string", "description": "The shell command to execute in the current runtime shell (for example, cmd.exe on Windows). Equal to 'cd {working_dir} && {your_command}'.", }, - "timeout": { - "type": "integer", - "description": "Optional timeout in seconds for the command execution.", - "default": 300, - }, "background": { "type": "boolean", "description": "Run the command in the background. Use the file read tool to read the output later.", "default": False, }, + "timeout": { + "type": "integer", + "description": "Optional timeout in seconds for the command execution.", + "default": 300, + }, "env": { "type": "object", "description": "Optional environment variables to set.", From 44e31bd49c366b11372c8c4eafe626349fcb4f88 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 22:27:18 +0800 Subject: [PATCH 6/9] feat(shell): implement background command execution with detached shell command support Co-authored-by: Copilot --- .../core/computer/booters/shell_background.py | 18 ++++++++++++++++++ astrbot/core/computer/booters/shipyard.py | 5 ++--- astrbot/core/computer/booters/shipyard_neo.py | 5 ++--- astrbot/core/tools/computer_tools/shell.py | 3 +-- 4 files changed, 23 insertions(+), 8 deletions(-) create mode 100644 astrbot/core/computer/booters/shell_background.py diff --git a/astrbot/core/computer/booters/shell_background.py b/astrbot/core/computer/booters/shell_background.py new file mode 100644 index 0000000000..6fe94c133a --- /dev/null +++ b/astrbot/core/computer/booters/shell_background.py @@ -0,0 +1,18 @@ +import shlex + +_BACKGROUND_SPAWN_SCRIPT = ( + "import subprocess, sys; " + "p = subprocess.Popen(" + "['bash', '-lc', sys.argv[1]], " + "stdin=subprocess.DEVNULL, " + "stdout=subprocess.DEVNULL, " + "stderr=subprocess.DEVNULL, " + "start_new_session=True, " + "close_fds=True" + "); " + "print(p.pid)" +) + + +def build_detached_shell_command(command: str) -> str: + return f"python3 -c {shlex.quote(_BACKGROUND_SPAWN_SCRIPT)} {shlex.quote(command)}" diff --git a/astrbot/core/computer/booters/shipyard.py b/astrbot/core/computer/booters/shipyard.py index 098511bbad..a8375544da 100644 --- a/astrbot/core/computer/booters/shipyard.py +++ b/astrbot/core/computer/booters/shipyard.py @@ -10,6 +10,7 @@ from ..olayer import FileSystemComponent, PythonComponent, ShellComponent from .base import ComputerBooter +from .shell_background import build_detached_shell_command from .shipyard_search_file_util import search_files_via_shell @@ -52,9 +53,7 @@ async def exec( run_command = f"{env_prefix} {run_command}" if background: - run_command = ( - f"nohup sh -lc {shlex.quote(run_command)} >/dev/null 2>&1 & echo $!" - ) + run_command = build_detached_shell_command(run_command) result = await self._shell.exec( run_command, diff --git a/astrbot/core/computer/booters/shipyard_neo.py b/astrbot/core/computer/booters/shipyard_neo.py index 813befe44f..cec3c3a366 100644 --- a/astrbot/core/computer/booters/shipyard_neo.py +++ b/astrbot/core/computer/booters/shipyard_neo.py @@ -13,6 +13,7 @@ ShellComponent, ) from .base import ComputerBooter +from .shell_background import build_detached_shell_command from .shipyard_search_file_util import search_files_via_shell try: @@ -116,9 +117,7 @@ async def exec( run_command = f"{env_prefix} {run_command}" if background: - run_command = ( - f"nohup sh -lc {shlex.quote(run_command)} >/dev/null 2>&1 & echo $!" - ) + run_command = build_detached_shell_command(run_command) result = await self._sandbox.shell.exec( run_command, diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index 11f1692c5c..e8fff4b50c 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -60,7 +60,7 @@ class ExecuteShellTool(FunctionTool): }, "background": { "type": "boolean", - "description": "Run the command in the background. Use the file read tool to read the output later.", + "description": "Run the command in the background. Use the file read tool to read the output later. For long running commands, using this option.", "default": False, }, "timeout": { @@ -123,7 +123,6 @@ async def call( timeout=timeout or 300, ) if stdout_file: - result["stdout_file"] = stdout_file result["stdout"] = ( f"Command is running in the background. stdout/stderr is being " f"written to `{stdout_file}`. Use astrbot_file_read_tool to read it." From 3f52c7aaa077e2f511505e1a305d5ebe67f99587 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Mon, 27 Apr 2026 22:29:45 +0800 Subject: [PATCH 7/9] test(shell): remove obsolete test for background shell command output redirection --- tests/unit/test_computer_shell_tool.py | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 tests/unit/test_computer_shell_tool.py diff --git a/tests/unit/test_computer_shell_tool.py b/tests/unit/test_computer_shell_tool.py deleted file mode 100644 index d8dfe4f8c0..0000000000 --- a/tests/unit/test_computer_shell_tool.py +++ /dev/null @@ -1,15 +0,0 @@ -from astrbot.core.tools.computer_tools.shell import ( - _redirect_background_stdout_command, -) - - -def test_background_shell_command_redirects_output_to_file(): - command = _redirect_background_stdout_command( - "python -c 'print(123)'", - output_path="/tmp/astrbot shell output.log", - local_runtime=False, - ) - - assert command == ( - "python -c 'print(123)' > \"/tmp/astrbot shell output.log\" 2>&1" - ) From 451d5450eed583334b2e34a0c3c74cbd73076d59 Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 28 Apr 2026 23:24:19 +0800 Subject: [PATCH 8/9] fix: reorder import statements in shell.py for consistency --- astrbot/core/tools/computer_tools/shell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index b2dce9725c..330b661cff 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -1,9 +1,9 @@ import json import os -import uuid -from pathlib import Path import shlex +import uuid from dataclasses import dataclass, field +from pathlib import Path from typing import Any from astrbot.api import FunctionTool From fddfbb946cf908d4e6388feb3bafcf69423d17bb Mon Sep 17 00:00:00 2001 From: Soulter <905617992@qq.com> Date: Tue, 28 Apr 2026 23:25:28 +0800 Subject: [PATCH 9/9] fix: wrap command in parentheses for background output redirection --- astrbot/core/tools/computer_tools/shell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/astrbot/core/tools/computer_tools/shell.py b/astrbot/core/tools/computer_tools/shell.py index 330b661cff..a6d44c2214 100644 --- a/astrbot/core/tools/computer_tools/shell.py +++ b/astrbot/core/tools/computer_tools/shell.py @@ -44,7 +44,7 @@ def _redirect_background_stdout_command( output_path: str, local_runtime: bool, ) -> str: - return f"{command} > {_quote_redirect_path(output_path, local_runtime=local_runtime)} 2>&1" + return f"({command}) > {_quote_redirect_path(output_path, local_runtime=local_runtime)} 2>&1" @builtin_tool(config=_COMPUTER_RUNTIME_TOOL_CONFIG)