diff --git a/.github/workflows/ghcr-build.yml b/.github/workflows/ghcr-build.yml index 384f45b1737f..a317c9a7ab03 100644 --- a/.github/workflows/ghcr-build.yml +++ b/.github/workflows/ghcr-build.yml @@ -233,7 +233,7 @@ jobs: run: pipx install poetry - name: Install Python dependencies using Poetry run: make install-python-dependencies - - name: Run runtime tests + - name: Run docker runtime tests run: | # We install pytest-xdist in order to run tests across CPUs poetry run pip install pytest-xdist diff --git a/openhands/runtime/README.md b/openhands/runtime/README.md index 3018433c1a67..5a4c1bd0f4fa 100644 --- a/openhands/runtime/README.md +++ b/openhands/runtime/README.md @@ -109,9 +109,27 @@ Key features: - Real-time logging and debugging capabilities - Direct access to the local file system - Faster execution due to local resources +- Container isolation for security This is the default runtime used within OpenHands. +### Local Runtime + +The Local Runtime is designed for direct execution on the local machine. Currently only supports running as the local user: + +- Runs the action_execution_server directly on the host +- No Docker container overhead +- Direct access to local system resources +- Ideal for development and testing when Docker is not available or desired + +Key features: +- Minimal setup required +- Direct access to local resources +- No container overhead +- Fastest execution speed + +**Important: This runtime provides no isolation as it runs directly on the host machine. All actions are executed with the same permissions as the user running OpenHands. For secure execution with proper isolation, use the Docker Runtime instead.** + ### Remote Runtime The Remote Runtime is designed for execution in a remote environment: diff --git a/openhands/runtime/__init__.py b/openhands/runtime/__init__.py index 9235380daa8d..5ddf881fcfa3 100644 --- a/openhands/runtime/__init__.py +++ b/openhands/runtime/__init__.py @@ -3,6 +3,7 @@ DockerRuntime, ) from openhands.runtime.impl.e2b.sandbox import E2BBox +from openhands.runtime.impl.local.local_runtime import LocalRuntime from openhands.runtime.impl.modal.modal_runtime import ModalRuntime from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime from openhands.runtime.impl.runloop.runloop_runtime import RunloopRuntime @@ -21,6 +22,8 @@ def get_runtime_cls(name: str): return ModalRuntime elif name == 'runloop': return RunloopRuntime + elif name == 'local': + return LocalRuntime else: raise ValueError(f'Runtime {name} not supported') diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index 8a5fcdc0edd9..2148ab2267d1 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -66,9 +66,6 @@ class ActionRequest(BaseModel): ROOT_GID = 0 -INIT_COMMANDS = [ - 'git config --global user.name "openhands" && git config --global user.email "openhands@all-hands.dev" && alias git="git --no-pager"', -] SESSION_API_KEY = os.environ.get('SESSION_API_KEY') api_key_header = APIKeyHeader(name='X-Session-API-Key', auto_error=False) @@ -163,6 +160,11 @@ async def _init_plugin(self, plugin: Plugin): ) async def _init_bash_commands(self): + INIT_COMMANDS = [ + 'git config --file ./.git_config user.name "openhands" && git config --file ./.git_config user.email "openhands@all-hands.dev" && alias git="git --no-pager" && export GIT_CONFIG=$(pwd)/.git_config' + if os.environ.get('LOCAL_RUNTIME_MODE') == '1' + else 'git config --global user.name "openhands" && git config --global user.email "openhands@all-hands.dev" && alias git="git --no-pager"' + ] logger.debug(f'Initializing by running {len(INIT_COMMANDS)} bash commands...') for command in INIT_COMMANDS: action = CmdRunAction(command=command) @@ -174,7 +176,6 @@ async def _init_bash_commands(self): f'Init command outputs (exit code: {obs.exit_code}): {obs.content}' ) assert obs.exit_code == 0 - logger.debug('Bash init commands completed') async def run_action(self, action) -> Observation: diff --git a/openhands/runtime/impl/local/local_runtime.py b/openhands/runtime/impl/local/local_runtime.py new file mode 100644 index 000000000000..547f2981d515 --- /dev/null +++ b/openhands/runtime/impl/local/local_runtime.py @@ -0,0 +1,342 @@ +""" +This runtime runs the action_execution_server directly on the local machine without Docker. +""" + +import os +import shutil +import subprocess +import tempfile +import threading +from typing import Callable, Optional + +import requests +import tenacity + +import openhands +from openhands.core.config import AppConfig +from openhands.core.exceptions import AgentRuntimeDisconnectedError +from openhands.core.logger import openhands_logger as logger +from openhands.events import EventStream +from openhands.events.action import ( + Action, +) +from openhands.events.observation import ( + Observation, +) +from openhands.events.serialization import event_to_dict, observation_from_dict +from openhands.runtime.impl.action_execution.action_execution_client import ( + ActionExecutionClient, +) +from openhands.runtime.impl.docker.docker_runtime import ( + APP_PORT_RANGE_1, + APP_PORT_RANGE_2, + EXECUTION_SERVER_PORT_RANGE, + VSCODE_PORT_RANGE, +) +from openhands.runtime.plugins import PluginRequirement +from openhands.runtime.utils import find_available_tcp_port +from openhands.runtime.utils.command import get_action_execution_server_startup_command +from openhands.utils.async_utils import call_sync_from_async +from openhands.utils.tenacity_stop import stop_if_should_exit + + +def check_dependencies(code_repo_path: str, poetry_venvs_path: str): + ERROR_MESSAGE = 'Please follow the instructions in https://github.com/All-Hands-AI/OpenHands/blob/main/Development.md to install OpenHands.' + if not os.path.exists(code_repo_path): + raise ValueError( + f'Code repo path {code_repo_path} does not exist. ' + ERROR_MESSAGE + ) + if not os.path.exists(poetry_venvs_path): + raise ValueError( + f'Poetry venvs path {poetry_venvs_path} does not exist. ' + ERROR_MESSAGE + ) + # Check jupyter is installed + logger.debug('Checking dependencies: Jupyter') + output = subprocess.check_output( + 'poetry run jupyter --version', + shell=True, + text=True, + cwd=code_repo_path, + ) + logger.debug(f'Jupyter output: {output}') + if 'jupyter' not in output.lower(): + raise ValueError('Jupyter is not properly installed. ' + ERROR_MESSAGE) + + # Check libtmux is installed + logger.debug('Checking dependencies: libtmux') + import libtmux + + server = libtmux.Server() + session = server.new_session(session_name='test-session') + pane = session.attached_pane + pane.send_keys('echo "test"') + pane_output = '\n'.join(pane.cmd('capture-pane', '-p').stdout) + session.kill_session() + if 'test' not in pane_output: + raise ValueError('libtmux is not properly installed. ' + ERROR_MESSAGE) + + # Check browser works + logger.debug('Checking dependencies: browser') + from openhands.runtime.browser.browser_env import BrowserEnv + + browser = BrowserEnv() + browser.close() + + +class LocalRuntime(ActionExecutionClient): + """This runtime will run the action_execution_server directly on the local machine. + When receiving an event, it will send the event to the server via HTTP. + + Args: + config (AppConfig): The application configuration. + event_stream (EventStream): The event stream to subscribe to. + sid (str, optional): The session ID. Defaults to 'default'. + plugins (list[PluginRequirement] | None, optional): List of plugin requirements. Defaults to None. + env_vars (dict[str, str] | None, optional): Environment variables to set. Defaults to None. + """ + + def __init__( + self, + config: AppConfig, + event_stream: EventStream, + sid: str = 'default', + plugins: list[PluginRequirement] | None = None, + env_vars: dict[str, str] | None = None, + status_callback: Callable | None = None, + attach_to_existing: bool = False, + headless_mode: bool = True, + ): + self.config = config + self._user_id = os.getuid() + self._username = os.getenv('USER') + + if self.config.workspace_base is not None: + logger.warning( + f'Workspace base path is set to {self.config.workspace_base}. ' + 'It will be used as the path for the agent to run in. ' + 'Be careful, the agent can EDIT files in this directory!' + ) + self.config.workspace_mount_path_in_sandbox = self.config.workspace_base + self._temp_workspace = None + else: + # A temporary directory is created for the agent to run in + # This is used for the local runtime only + self._temp_workspace = tempfile.mkdtemp( + prefix=f'openhands_workspace_{sid}', + ) + self.config.workspace_mount_path_in_sandbox = self._temp_workspace + + logger.warning( + 'Initializing LocalRuntime. WARNING: NO SANDBOX IS USED. ' + 'This is an experimental feature, please report issues to https://github.com/All-Hands-AI/OpenHands/issues. ' + '`run_as_openhands` will be ignored since the current user will be used to launch the server. ' + 'We highly recommend using a sandbox (eg. DockerRuntime) unless you ' + 'are running in a controlled environment.\n' + f'Temp workspace: {self._temp_workspace}. ' + f'User ID: {self._user_id}. ' + f'Username: {self._username}.' + ) + + if self.config.workspace_base is not None: + logger.warning( + f'Workspace base path is set to {self.config.workspace_base}. It will be used as the path for the agent to run in.' + ) + self.config.workspace_mount_path_in_sandbox = self.config.workspace_base + else: + logger.warning( + 'Workspace base path is NOT set. Agent will run in a temporary directory.' + ) + self._temp_workspace = tempfile.mkdtemp() + self.config.workspace_mount_path_in_sandbox = self._temp_workspace + + self._host_port = -1 + self._vscode_port = -1 + self._app_ports: list[int] = [] + + self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._host_port}' + self.status_callback = status_callback + self.server_process: Optional[subprocess.Popen[str]] = None + self.action_semaphore = threading.Semaphore(1) # Ensure one action at a time + + # Update env vars + if self.config.sandbox.runtime_startup_env_vars: + os.environ.update(self.config.sandbox.runtime_startup_env_vars) + + # Initialize the action_execution_server + super().__init__( + config, + event_stream, + sid, + plugins, + env_vars, + status_callback, + attach_to_existing, + headless_mode, + ) + + def _get_action_execution_server_host(self): + return self.api_url + + async def connect(self): + """Start the action_execution_server on the local machine.""" + self.send_status_message('STATUS$STARTING_RUNTIME') + + self._host_port = self._find_available_port(EXECUTION_SERVER_PORT_RANGE) + self._vscode_port = self._find_available_port(VSCODE_PORT_RANGE) + self._app_ports = [ + self._find_available_port(APP_PORT_RANGE_1), + self._find_available_port(APP_PORT_RANGE_2), + ] + self.api_url = f'{self.config.sandbox.local_runtime_url}:{self._host_port}' + + # Start the server process + cmd = get_action_execution_server_startup_command( + server_port=self._host_port, + plugins=self.plugins, + app_config=self.config, + python_prefix=[], + override_user_id=self._user_id, + override_username=self._username, + ) + + self.log('debug', f'Starting server with command: {cmd}') + env = os.environ.copy() + # Get the code repo path + code_repo_path = os.path.dirname(os.path.dirname(openhands.__file__)) + env['PYTHONPATH'] = f'{code_repo_path}:$PYTHONPATH' + env['OPENHANDS_REPO_PATH'] = code_repo_path + env['LOCAL_RUNTIME_MODE'] = '1' + # run poetry show -v | head -n 1 | awk '{print $2}' + poetry_venvs_path = ( + subprocess.check_output( + ['poetry', 'show', '-v'], + env=env, + cwd=code_repo_path, + text=True, + shell=False, + ) + .splitlines()[0] + .split(':')[1] + .strip() + ) + env['POETRY_VIRTUALENVS_PATH'] = poetry_venvs_path + logger.debug(f'POETRY_VIRTUALENVS_PATH: {poetry_venvs_path}') + + check_dependencies(code_repo_path, poetry_venvs_path) + self.server_process = subprocess.Popen( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + bufsize=1, + env=env, + ) + + # Start a thread to read and log server output + def log_output(): + while ( + self.server_process + and self.server_process.poll() + and self.server_process.stdout + ): + line = self.server_process.stdout.readline() + if not line: + break + self.log('debug', f'Server: {line.strip()}') + + self._log_thread = threading.Thread(target=log_output, daemon=True) + self._log_thread.start() + + self.log('info', f'Waiting for server to become ready at {self.api_url}...') + self.send_status_message('STATUS$WAITING_FOR_CLIENT') + + await call_sync_from_async(self._wait_until_alive) + + if not self.attach_to_existing: + await call_sync_from_async(self.setup_initial_env) + + self.log( + 'debug', + f'Server initialized with plugins: {[plugin.name for plugin in self.plugins]}', + ) + if not self.attach_to_existing: + self.send_status_message(' ') + self._runtime_initialized = True + + def _find_available_port(self, port_range, max_attempts=5): + port = port_range[1] + for _ in range(max_attempts): + port = find_available_tcp_port(port_range[0], port_range[1]) + return port + return port + + @tenacity.retry( + wait=tenacity.wait_exponential(min=1, max=10), + stop=tenacity.stop_after_attempt(10) | stop_if_should_exit(), + before_sleep=lambda retry_state: logger.debug( + f'Waiting for server to be ready... (attempt {retry_state.attempt_number})' + ), + ) + def _wait_until_alive(self): + """Wait until the server is ready to accept requests.""" + if self.server_process and self.server_process.poll() is not None: + raise RuntimeError('Server process died') + + try: + response = self.session.get(f'{self.api_url}/alive') + response.raise_for_status() + return True + except Exception as e: + self.log('debug', f'Server not ready yet: {e}') + raise + + async def execute_action(self, action: Action) -> Observation: + """Execute an action by sending it to the server.""" + if not self._runtime_initialized: + raise AgentRuntimeDisconnectedError('Runtime not initialized') + + if self.server_process is None or self.server_process.poll() is not None: + raise AgentRuntimeDisconnectedError('Server process died') + + with self.action_semaphore: + try: + response = await call_sync_from_async( + lambda: self.session.post( + f'{self.api_url}/execute_action', + json={'action': event_to_dict(action)}, + ) + ) + return observation_from_dict(response.json()) + except requests.exceptions.ConnectionError: + raise AgentRuntimeDisconnectedError('Server connection lost') + + def close(self): + """Stop the server process.""" + if self.server_process: + self.server_process.terminate() + try: + self.server_process.wait(timeout=5) + except subprocess.TimeoutExpired: + self.server_process.kill() + self.server_process = None + self._log_thread.join() + + if self._temp_workspace: + shutil.rmtree(self._temp_workspace) + + super().close() + + @property + def vscode_url(self) -> str | None: + token = super().get_vscode_token() + if not token: + return None + vscode_url = f'http://localhost:{self._vscode_port}/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}' + return vscode_url + + @property + def web_hosts(self): + hosts: dict[str, int] = {} + for port in self._app_ports: + hosts[f'http://localhost:{port}'] = port + return hosts diff --git a/openhands/runtime/plugins/jupyter/__init__.py b/openhands/runtime/plugins/jupyter/__init__.py index 23128b60a8f9..dd4962b372f5 100644 --- a/openhands/runtime/plugins/jupyter/__init__.py +++ b/openhands/runtime/plugins/jupyter/__init__.py @@ -1,3 +1,4 @@ +import os import subprocess import time from dataclasses import dataclass @@ -22,19 +23,46 @@ class JupyterPlugin(Plugin): async def initialize(self, username: str, kernel_id: str = 'openhands-default'): self.kernel_gateway_port = find_available_tcp_port(40000, 49999) self.kernel_id = kernel_id - self.gateway_process = subprocess.Popen( - ( - f"su - {username} -s /bin/bash << 'EOF'\n" + if username in ['root', 'openhands']: + # Non-LocalRuntime + prefix = f'su - {username} -s ' + # cd to code repo, setup all env vars and run micromamba + poetry_prefix = ( 'cd /openhands/code\n' 'export POETRY_VIRTUALENVS_PATH=/openhands/poetry;\n' 'export PYTHONPATH=/openhands/code:$PYTHONPATH;\n' 'export MAMBA_ROOT_PREFIX=/openhands/micromamba;\n' '/openhands/micromamba/bin/micromamba run -n openhands ' - 'poetry run jupyter kernelgateway ' - '--KernelGatewayApp.ip=0.0.0.0 ' - f'--KernelGatewayApp.port={self.kernel_gateway_port}\n' - 'EOF' - ), + ) + else: + # LocalRuntime + prefix = '' + code_repo_path = os.environ.get('OPENHANDS_REPO_PATH') + if not code_repo_path: + raise ValueError( + 'OPENHANDS_REPO_PATH environment variable is not set. ' + 'This is required for the jupyter plugin to work with LocalRuntime.' + ) + # assert POETRY_VIRTUALENVS_PATH is set + poetry_venvs_path = os.environ.get('POETRY_VIRTUALENVS_PATH') + if not poetry_venvs_path: + raise ValueError( + 'POETRY_VIRTUALENVS_PATH environment variable is not set. ' + 'This is required for the jupyter plugin to work with LocalRuntime.' + ) + poetry_prefix = f'cd {code_repo_path}\n' + jupyter_launch_command = ( + f"{prefix}/bin/bash << 'EOF'\n" + f'{poetry_prefix}' + 'poetry run jupyter kernelgateway ' + '--KernelGatewayApp.ip=0.0.0.0 ' + f'--KernelGatewayApp.port={self.kernel_gateway_port}\n' + 'EOF' + ) + logger.debug(f'Jupyter launch command: {jupyter_launch_command}') + + self.gateway_process = subprocess.Popen( + jupyter_launch_command, stderr=subprocess.STDOUT, shell=True, ) diff --git a/openhands/runtime/plugins/vscode/__init__.py b/openhands/runtime/plugins/vscode/__init__.py index cb1052dd45b8..6765432545a4 100644 --- a/openhands/runtime/plugins/vscode/__init__.py +++ b/openhands/runtime/plugins/vscode/__init__.py @@ -19,6 +19,15 @@ class VSCodePlugin(Plugin): name: str = 'vscode' async def initialize(self, username: str): + if username not in ['root', 'openhands']: + self.vscode_port = None + self.vscode_connection_token = None + logger.warning( + 'VSCodePlugin is only supported for root or openhands user. ' + 'It is not yet supported for other users (i.e., when running LocalRuntime).' + ) + return + self.vscode_port = int(os.environ['VSCODE_PORT']) self.vscode_connection_token = str(uuid.uuid4()) assert check_port_available(self.vscode_port) diff --git a/openhands/runtime/utils/bash.py b/openhands/runtime/utils/bash.py index 9da848c44dcc..5fda883d4d01 100644 --- a/openhands/runtime/utils/bash.py +++ b/openhands/runtime/utils/bash.py @@ -184,9 +184,10 @@ def __init__( def initialize(self): self.server = libtmux.Server() window_command = '/bin/bash' - if self.username: + if self.username in ['root', 'openhands']: # This starts a non-login (new) shell for the given user window_command = f'su {self.username} -' + # otherwise, we are running as the CURRENT USER (e.g., when running LocalRuntime) session_name = f'openhands-{self.username}-{uuid.uuid4()}' self.session = self.server.new_session( diff --git a/openhands/runtime/utils/command.py b/openhands/runtime/utils/command.py index 76722daca476..17458c1f3ee0 100644 --- a/openhands/runtime/utils/command.py +++ b/openhands/runtime/utils/command.py @@ -17,6 +17,8 @@ def get_action_execution_server_startup_command( app_config: AppConfig, python_prefix: list[str] = DEFAULT_PYTHON_PREFIX, use_nice_for_root: bool = True, + override_user_id: int | None = None, + override_username: str | None = None, ): sandbox_config = app_config.sandbox @@ -32,7 +34,13 @@ def get_action_execution_server_startup_command( '--browsergym-eval-env' ] + sandbox_config.browsergym_eval_env.split(' ') - is_root = not app_config.run_as_openhands + username = override_username or ( + 'openhands' if app_config.run_as_openhands else 'root' + ) + user_id = override_user_id or ( + sandbox_config.user_id if app_config.run_as_openhands else 0 + ) + is_root = bool(username == 'root') base_cmd = [ *python_prefix, @@ -45,9 +53,9 @@ def get_action_execution_server_startup_command( app_config.workspace_mount_path_in_sandbox, *plugin_args, '--username', - 'openhands' if app_config.run_as_openhands else 'root', + username, '--user-id', - str(sandbox_config.user_id), + str(user_id), *browsergym_args, ] diff --git a/openhands/runtime/utils/runtime_init.py b/openhands/runtime/utils/runtime_init.py index b38ab7ca3495..511a523831fa 100644 --- a/openhands/runtime/utils/runtime_init.py +++ b/openhands/runtime/utils/runtime_init.py @@ -1,3 +1,4 @@ +import os import subprocess from openhands.core.logger import openhands_logger as logger @@ -31,6 +32,10 @@ def init_user_and_working_directory( Returns: int | None: The user ID if it was updated, None otherwise. """ + # if username is CURRENT_USER, then we don't need to do anything + # This is specific to the local runtime + if username == os.getenv('USER') and username not in ['root', 'openhands']: + return None # First create the working directory, independent of the user logger.debug(f'Client working directory: {initial_cwd}') diff --git a/poetry.lock b/poetry.lock index df1eb3cd5a59..ef37bd55ce1b 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,4 @@ -# This file is automatically @generated by Poetry 2.0.0 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.0.1 and should not be changed by hand. [[package]] name = "aiohappyeyeballs" @@ -228,7 +228,7 @@ version = "0.1.4" description = "Disable App Nap on macOS >= 10.9" optional = false python-versions = ">=3.6" -groups = ["runtime"] +groups = ["main", "runtime"] markers = "platform_system == \"Darwin\"" files = [ {file = "appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c"}, @@ -335,7 +335,7 @@ version = "3.0.0" description = "Annotate AST trees with source code positions" optional = false python-versions = ">=3.8" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2"}, {file = "asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7"}, @@ -1141,7 +1141,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\"", dev = "os_name == \"nt\"", evaluation = "platform_system == \"Windows\" or sys_platform == \"win32\"", llama-index = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\"", dev = "os_name == \"nt\"", evaluation = "platform_system == \"Windows\" or sys_platform == \"win32\"", llama-index = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "coloredlogs" @@ -1167,7 +1167,7 @@ version = "0.2.2" description = "Jupyter Python Comm implementation, for usage in ipykernel, xeus-python etc." optional = false python-versions = ">=3.8" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3"}, {file = "comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e"}, @@ -1370,7 +1370,6 @@ files = [ {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:761817a3377ef15ac23cd7834715081791d4ec77f9297ee694ca1ee9c2c7e5eb"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3c672a53c0fb4725a29c303be906d3c1fa99c32f58abe008a82705f9ee96f40b"}, {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4ac4c9f37eba52cb6fbeaf5b59c152ea976726b865bd4cf87883a7e7006cc543"}, - {file = "cryptography-44.0.0-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:60eb32934076fa07e4316b7b2742fa52cbb190b42c2df2863dbc4230a0a9b385"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ed3534eb1090483c96178fcb0f8893719d96d5274dfde98aa6add34614e97c8e"}, {file = "cryptography-44.0.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f3f6fdfa89ee2d9d496e2c087cebef9d4fcbb0ad63c40e821b39f74bf48d9c5e"}, {file = "cryptography-44.0.0-cp37-abi3-win32.whl", hash = "sha256:eb33480f1bad5b78233b0ad3e1b0be21e8ef1da745d8d2aecbb20671658b9053"}, @@ -1381,7 +1380,6 @@ files = [ {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:c5eb858beed7835e5ad1faba59e865109f3e52b3783b9ac21e7e47dc5554e289"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:f53c2c87e0fb4b0c00fa9571082a057e37690a8f12233306161c8f4b819960b7"}, {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9e6fc8a08e116fb7c7dd1f040074c9d7b51d74a8ea40d4df2fc7aa08b76b9e6c"}, - {file = "cryptography-44.0.0-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:9abcc2e083cbe8dde89124a47e5e53ec38751f0d7dfd36801008f316a127d7ba"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:d2436114e46b36d00f8b72ff57e598978b37399d2786fd39793c36c6d5cb1c64"}, {file = "cryptography-44.0.0-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a01956ddfa0a6790d594f5b34fc1bfa6098aca434696a03cfdbe469b8ed79285"}, {file = "cryptography-44.0.0-cp39-abi3-win32.whl", hash = "sha256:eca27345e1214d1b9f9490d200f9db5a874479be914199194e746c893788d417"}, @@ -1489,7 +1487,7 @@ version = "1.8.11" description = "An implementation of the Debug Adapter Protocol for Python" optional = false python-versions = ">=3.8" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "debugpy-1.8.11-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:2b26fefc4e31ff85593d68b9022e35e8925714a10ab4858fb1b577a8a48cb8cd"}, {file = "debugpy-1.8.11-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:61bc8b3b265e6949855300e84dc93d02d7a3a637f2aec6d382afd4ceb9120c9f"}, @@ -1525,7 +1523,7 @@ version = "5.1.1" description = "Decorators for Humans" optional = false python-versions = ">=3.5" -groups = ["evaluation", "runtime"] +groups = ["main", "evaluation", "runtime"] files = [ {file = "decorator-5.1.1-py3-none-any.whl", hash = "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186"}, {file = "decorator-5.1.1.tar.gz", hash = "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330"}, @@ -1776,7 +1774,7 @@ version = "2.1.0" description = "Get the currently executing AST node of a frame, and other information" optional = false python-versions = ">=3.8" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "executing-2.1.0-py2.py3-none-any.whl", hash = "sha256:8d63781349375b5ebccc3142f4b30350c0cd9c79f921cde38be2be4637e98eaf"}, {file = "executing-2.1.0.tar.gz", hash = "sha256:8ea27ddd260da8150fa5a708269c4a10e76161e2496ec3e587da9e3c0fe4b9ab"}, @@ -3305,7 +3303,7 @@ version = "6.29.5" description = "IPython Kernel for Jupyter" optional = false python-versions = ">=3.8" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5"}, {file = "ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215"}, @@ -3339,7 +3337,7 @@ version = "8.31.0" description = "IPython: Productive Interactive Computing" optional = false python-versions = ">=3.10" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "ipython-8.31.0-py3-none-any.whl", hash = "sha256:46ec58f8d3d076a61d128fe517a51eb730e3aaf0c184ea8c17d16e366660c6a6"}, {file = "ipython-8.31.0.tar.gz", hash = "sha256:b6a2274606bec6166405ff05e54932ed6e5cfecaca1fc05f2cacde7bb074d70b"}, @@ -3370,6 +3368,28 @@ qtconsole = ["qtconsole"] test = ["packaging", "pickleshare", "pytest", "pytest-asyncio (<0.22)", "testpath"] test-extra = ["curio", "ipython[test]", "matplotlib (!=3.2.0)", "nbformat", "numpy (>=1.23)", "pandas", "trio"] +[[package]] +name = "ipywidgets" +version = "8.1.5" +description = "Jupyter interactive widgets" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "ipywidgets-8.1.5-py3-none-any.whl", hash = "sha256:3290f526f87ae6e77655555baba4f36681c555b8bdbbff430b70e52c34c86245"}, + {file = "ipywidgets-8.1.5.tar.gz", hash = "sha256:870e43b1a35656a80c18c9503bbf2d16802db1cb487eec6fab27d683381dde17"}, +] + +[package.dependencies] +comm = ">=0.1.3" +ipython = ">=6.1.0" +jupyterlab-widgets = ">=3.0.12,<3.1.0" +traitlets = ">=4.3.1" +widgetsnbextension = ">=4.0.12,<4.1.0" + +[package.extras] +test = ["ipykernel", "jsonschema", "pytest (>=3.6.0)", "pytest-cov", "pytz"] + [[package]] name = "isoduration" version = "20.11.0" @@ -3403,7 +3423,7 @@ version = "0.19.2" description = "An autocompletion tool for Python that can be used for text editors." optional = false python-versions = ">=3.6" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9"}, {file = "jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0"}, @@ -3635,7 +3655,7 @@ version = "8.6.3" description = "Jupyter protocol implementation and client libraries" optional = false python-versions = ">=3.8" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f"}, {file = "jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419"}, @@ -3658,7 +3678,7 @@ version = "5.7.2" description = "Jupyter core package. A base package on which Jupyter projects rely." optional = false python-versions = ">=3.8" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "jupyter_core-5.7.2-py3-none-any.whl", hash = "sha256:4f7315d2f6b4bcf2e3e7cb6e46772eba760ae459cd1f59d29eb57b0a01bd7409"}, {file = "jupyter_core-5.7.2.tar.gz", hash = "sha256:aa5f8d32bbf6b431ac830496da7392035d6f61b4f54872f15c4bd2a9c3f536d9"}, @@ -3867,6 +3887,18 @@ docs = ["autodoc-traits", "jinja2 (<3.2.0)", "mistune (<4)", "myst-parser", "pyd openapi = ["openapi-core (>=0.18.0,<0.19.0)", "ruamel-yaml"] test = ["hatch", "ipykernel", "openapi-core (>=0.18.0,<0.19.0)", "openapi-spec-validator (>=0.6.0,<0.8.0)", "pytest (>=7.0,<8)", "pytest-console-scripts", "pytest-cov", "pytest-jupyter[server] (>=0.6.2)", "pytest-timeout", "requests-mock", "ruamel-yaml", "sphinxcontrib-spelling", "strict-rfc3339", "werkzeug"] +[[package]] +name = "jupyterlab-widgets" +version = "3.0.13" +description = "Jupyter interactive widgets for JupyterLab" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "jupyterlab_widgets-3.0.13-py3-none-any.whl", hash = "sha256:e3cda2c233ce144192f1e29914ad522b2f4c40e77214b0cc97377ca3d323db54"}, + {file = "jupyterlab_widgets-3.0.13.tar.gz", hash = "sha256:a2966d385328c1942b683a8cd96b89b8dd82c8b8f81dda902bb2bc06d46f5bed"}, +] + [[package]] name = "kiwisolver" version = "1.4.8" @@ -4802,7 +4834,7 @@ version = "0.1.7" description = "Inline Matplotlib backend for Jupyter" optional = false python-versions = ">=3.8" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca"}, {file = "matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90"}, @@ -5386,7 +5418,7 @@ version = "1.6.0" description = "Patch asyncio to allow nested event loops" optional = false python-versions = ">=3.5" -groups = ["llama-index", "runtime"] +groups = ["main", "llama-index", "runtime"] files = [ {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, @@ -6241,7 +6273,7 @@ version = "0.8.4" description = "A Python Parser" optional = false python-versions = ">=3.6" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18"}, {file = "parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d"}, @@ -6373,7 +6405,7 @@ version = "4.3.6" description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false python-versions = ">=3.8" -groups = ["dev", "evaluation", "runtime"] +groups = ["main", "dev", "evaluation", "runtime"] files = [ {file = "platformdirs-4.3.6-py3-none-any.whl", hash = "sha256:73e575e1408ab8103900836b97580d5307456908a03e92031bab39e4554cc3fb"}, {file = "platformdirs-4.3.6.tar.gz", hash = "sha256:357fb2acbc885b0419afd3ce3ed34564c13c9b95c89360cd9563f73aa5e2b907"}, @@ -6505,7 +6537,7 @@ version = "3.0.48" description = "Library for building powerful interactive command lines in Python" optional = false python-versions = ">=3.7.0" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "prompt_toolkit-3.0.48-py3-none-any.whl", hash = "sha256:f49a827f90062e411f1ce1f854f2aedb3c23353244f8108b89283587397ac10e"}, {file = "prompt_toolkit-3.0.48.tar.gz", hash = "sha256:d6623ab0477a80df74e646bdbc93621143f5caf104206aa29294d53de1a03d90"}, @@ -6651,7 +6683,7 @@ version = "6.1.1" description = "Cross-platform lib for process and system monitoring in Python." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,>=2.7" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "psutil-6.1.1-cp27-cp27m-macosx_10_9_x86_64.whl", hash = "sha256:9ccc4316f24409159897799b83004cb1e24f9819b0dcf9c0b68bdcb6cefee6a8"}, {file = "psutil-6.1.1-cp27-cp27m-manylinux2010_i686.whl", hash = "sha256:ca9609c77ea3b8481ab005da74ed894035936223422dc591d6772b147421f777"}, @@ -6694,7 +6726,7 @@ version = "0.2.3" description = "Safely evaluate AST nodes without side effects" optional = false python-versions = "*" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0"}, {file = "pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42"}, @@ -7658,7 +7690,7 @@ version = "26.2.0" description = "Python bindings for 0MQ" optional = false python-versions = ">=3.7" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_15_universal2.whl", hash = "sha256:ddf33d97d2f52d89f6e6e7ae66ee35a4d9ca6f36eda89c24591b0c40205a3629"}, {file = "pyzmq-26.2.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dacd995031a01d16eec825bf30802fceb2c3791ef24bcce48fa98ce40918c27b"}, @@ -7774,6 +7806,49 @@ files = [ [package.dependencies] cffi = {version = "*", markers = "implementation_name == \"pypy\""} +[[package]] +name = "qtconsole" +version = "5.6.1" +description = "Jupyter Qt console" +optional = false +python-versions = ">=3.8" +groups = ["main"] +files = [ + {file = "qtconsole-5.6.1-py3-none-any.whl", hash = "sha256:3d22490d9589bace566ad4f3455b61fa2209156f40e87e19e2c3cb64e9264950"}, + {file = "qtconsole-5.6.1.tar.gz", hash = "sha256:5cad1c7e6c75d3ef8143857fd2ed28062b4b92b933c2cc328252d18a9cfd0be5"}, +] + +[package.dependencies] +ipykernel = ">=4.1" +jupyter-client = ">=4.1" +jupyter-core = "*" +packaging = "*" +pygments = "*" +qtpy = ">=2.4.0" +traitlets = "<5.2.1 || >5.2.1,<5.2.2 || >5.2.2" + +[package.extras] +doc = ["Sphinx (>=1.3)"] +test = ["flaky", "pytest", "pytest-qt"] + +[[package]] +name = "qtpy" +version = "2.4.2" +description = "Provides an abstraction layer on top of the various Qt bindings (PyQt5/6 and PySide2/6)." +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "QtPy-2.4.2-py3-none-any.whl", hash = "sha256:5a696b1dd7a354cb330657da1d17c20c2190c72d4888ba923f8461da67aa1a1c"}, + {file = "qtpy-2.4.2.tar.gz", hash = "sha256:9d6ec91a587cc1495eaebd23130f7619afa5cdd34a277acb87735b4ad7c65156"}, +] + +[package.dependencies] +packaging = "*" + +[package.extras] +test = ["pytest (>=6,!=7.0.0,!=7.0.1)", "pytest-cov (>=3.0.0)", "pytest-qt"] + [[package]] name = "redis" version = "5.2.1" @@ -8815,7 +8890,7 @@ version = "0.6.3" description = "Extract data from python stack frames and tracebacks for informative displays" optional = false python-versions = "*" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695"}, {file = "stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9"}, @@ -9313,7 +9388,7 @@ version = "5.14.3" description = "Traitlets Python configuration system" optional = false python-versions = ">=3.8" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f"}, {file = "traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7"}, @@ -9970,7 +10045,7 @@ version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" optional = false python-versions = "*" -groups = ["runtime"] +groups = ["main", "runtime"] files = [ {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, @@ -10126,6 +10201,18 @@ files = [ {file = "whatthepatch-1.0.7.tar.gz", hash = "sha256:9eefb4ebea5200408e02d413d2b4bc28daea6b78bb4b4d53431af7245f7d7edf"}, ] +[[package]] +name = "widgetsnbextension" +version = "4.0.13" +description = "Jupyter interactive widgets for Jupyter Notebook" +optional = false +python-versions = ">=3.7" +groups = ["main"] +files = [ + {file = "widgetsnbextension-4.0.13-py3-none-any.whl", hash = "sha256:74b2692e8500525cc38c2b877236ba51d34541e6385eeed5aec15a70f88a6c71"}, + {file = "widgetsnbextension-4.0.13.tar.gz", hash = "sha256:ffcb67bc9febd10234a362795f643927f4e0c05d9342c727b65d2384f8feacb6"}, +] + [[package]] name = "wrapt" version = "1.17.0" @@ -10555,4 +10642,4 @@ testing = ["coverage[toml]", "zope.event", "zope.testing"] [metadata] lock-version = "2.1" python-versions = "^3.12" -content-hash = "41b54f52d12ebf5a1cdaad9d1fa992f0debf47d5276a7c2a5d7a9644b1875570" +content-hash = "cb3fab7a5e6d48140970edc84b14918bbbd637cdfdefd0a88462e4db42fb1d6f" diff --git a/pyproject.toml b/pyproject.toml index cd57a66261d5..918f16634263 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -69,6 +69,8 @@ openhands-aci = "^0.2.0" python-socketio = "^5.11.4" redis = "^5.2.0" sse-starlette = "^2.1.3" +ipywidgets = "^8.1.5" +qtconsole = "^5.6.1" [tool.poetry.group.llama-index.dependencies] llama-index = "*" @@ -101,6 +103,7 @@ reportlab = "*" [tool.coverage.run] concurrency = ["gevent"] + [tool.poetry.group.runtime.dependencies] jupyterlab = "*" notebook = "*" @@ -129,6 +132,7 @@ ignore = ["D1"] [tool.ruff.lint.pydocstyle] convention = "google" + [tool.poetry.group.evaluation.dependencies] streamlit = "*" whatthepatch = "*" diff --git a/tests/runtime/conftest.py b/tests/runtime/conftest.py index 8e5119e63189..73b18680a11e 100644 --- a/tests/runtime/conftest.py +++ b/tests/runtime/conftest.py @@ -3,16 +3,16 @@ import shutil import stat import time -from pathlib import Path import pytest from pytest import TempPathFactory -from openhands.core.config import load_app_config +from openhands.core.config import AppConfig, load_app_config from openhands.core.logger import openhands_logger as logger from openhands.events import EventStream from openhands.runtime.base import Runtime from openhands.runtime.impl.docker.docker_runtime import DockerRuntime +from openhands.runtime.impl.local.local_runtime import LocalRuntime from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime from openhands.runtime.impl.runloop.runloop_runtime import RunloopRuntime from openhands.runtime.plugins import AgentSkillsRequirement, JupyterRequirement @@ -38,13 +38,6 @@ def _get_host_folder(runtime: Runtime) -> str: return runtime.config.workspace_mount_path -def _get_sandbox_folder(runtime: Runtime) -> Path | None: - sid = _get_runtime_sid(runtime) - if sid: - return Path(os.path.join(sandbox_test_folder, sid)) - return None - - def _remove_folder(folder: str) -> bool: success = False if folder and os.path.isdir(folder): @@ -131,6 +124,8 @@ def get_runtime_classes() -> list[type[Runtime]]: runtime = TEST_RUNTIME if runtime.lower() == 'docker' or runtime.lower() == 'eventstream': return [DockerRuntime] + elif runtime.lower() == 'local': + return [LocalRuntime] elif runtime.lower() == 'remote': return [RemoteRuntime] elif runtime.lower() == 'runloop': @@ -216,7 +211,7 @@ def _load_runtime( force_rebuild_runtime: bool = False, runtime_startup_env_vars: dict[str, str] | None = None, docker_runtime_kwargs: dict[str, str] | None = None, -) -> Runtime: +) -> tuple[Runtime, AppConfig]: sid = 'rt_' + str(random.randint(100000, 999999)) # AgentSkills need to be initialized **before** Jupyter @@ -269,13 +264,12 @@ def _load_runtime( ) call_async_from_sync(runtime.connect) time.sleep(2) - return runtime + return runtime, config # Export necessary function __all__ = [ '_load_runtime', '_get_host_folder', - '_get_sandbox_folder', '_remove_folder', ] diff --git a/tests/runtime/test_bash.py b/tests/runtime/test_bash.py index d107cc9569c8..48dae5c422c9 100644 --- a/tests/runtime/test_bash.py +++ b/tests/runtime/test_bash.py @@ -7,14 +7,13 @@ import pytest from conftest import ( _close_test_runtime, - _get_sandbox_folder, _load_runtime, ) from openhands.core.logger import openhands_logger as logger from openhands.events.action import CmdRunAction from openhands.events.observation import CmdOutputObservation, ErrorObservation -from openhands.runtime.base import Runtime +from openhands.runtime.impl.local.local_runtime import LocalRuntime # ============================================================================================================================ # Bash-specific tests @@ -31,7 +30,7 @@ def _run_cmd_action(runtime, custom_command: str): def test_bash_command_env(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: obs = runtime.run_action(CmdRunAction(command='env')) assert isinstance( @@ -43,7 +42,7 @@ def test_bash_command_env(temp_dir, runtime_cls, run_as_openhands): def test_bash_server(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: action = CmdRunAction(command='python3 -m http.server 8080') action.set_hard_timeout(1) @@ -64,7 +63,7 @@ def test_bash_server(temp_dir, runtime_cls, run_as_openhands): assert isinstance(obs, CmdOutputObservation) assert obs.exit_code == 0 assert 'Keyboard interrupt received, exiting.' in obs.content - assert '/workspace' in obs.metadata.working_dir + assert config.workspace_mount_path_in_sandbox in obs.metadata.working_dir action = CmdRunAction(command='ls') action.set_hard_timeout(1) @@ -73,7 +72,7 @@ def test_bash_server(temp_dir, runtime_cls, run_as_openhands): assert isinstance(obs, CmdOutputObservation) assert obs.exit_code == 0 assert 'Keyboard interrupt received, exiting.' not in obs.content - assert '/workspace' in obs.metadata.working_dir + assert config.workspace_mount_path_in_sandbox in obs.metadata.working_dir # run it again! action = CmdRunAction(command='python3 -m http.server 8080') @@ -89,7 +88,7 @@ def test_bash_server(temp_dir, runtime_cls, run_as_openhands): def test_multiline_commands(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: # single multiline command obs = _run_cmd_action(runtime, 'echo \\\n -e "foo"') @@ -123,7 +122,7 @@ def test_multiple_multiline_commands(temp_dir, runtime_cls, run_as_openhands): ] joined_cmds = '\n'.join(cmds) - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # First test that running multiple commands at once fails obs = _run_cmd_action(runtime, joined_cmds) @@ -157,7 +156,7 @@ def test_multiple_multiline_commands(temp_dir, runtime_cls, run_as_openhands): def test_complex_commands(temp_dir, runtime_cls): cmd = """count=0; tries=0; while [ $count -lt 3 ]; do result=$(echo "Heads"); tries=$((tries+1)); echo "Flip $tries: $result"; if [ "$result" = "Heads" ]; then count=$((count+1)); else count=0; fi; done; echo "Got 3 heads in a row after $tries flips!";""" - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: obs = _run_cmd_action(runtime, cmd) logger.info(obs, extra={'msg_type': 'OBSERVATION'}) @@ -170,7 +169,7 @@ def test_complex_commands(temp_dir, runtime_cls): def test_no_ps2_in_output(temp_dir, runtime_cls, run_as_openhands): """Test that the PS2 sign is not added to the output of a multiline command.""" - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: obs = _run_cmd_action(runtime, 'echo -e "hello\nworld"') assert obs.exit_code == 0, 'The exit code should be 0.' @@ -195,7 +194,7 @@ def test_multiline_command_loop(temp_dir, runtime_cls): mv "$file" "$new_date" done && echo "success" """ - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: obs = _run_cmd_action(runtime, init_cmd) assert obs.exit_code == 0, 'The exit code should be 0.' @@ -209,9 +208,11 @@ def test_multiline_command_loop(temp_dir, runtime_cls): def test_cmd_run(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: - obs = _run_cmd_action(runtime, 'ls -l /workspace') + obs = _run_cmd_action( + runtime, f'ls -l {config.workspace_mount_path_in_sandbox}' + ) assert obs.exit_code == 0 obs = _run_cmd_action(runtime, 'ls -l') @@ -225,6 +226,8 @@ def test_cmd_run(temp_dir, runtime_cls, run_as_openhands): assert obs.exit_code == 0 if run_as_openhands: assert 'openhands' in obs.content + elif runtime_cls == LocalRuntime: + assert 'root' not in obs.content and 'openhands' not in obs.content else: assert 'root' in obs.content assert 'test' in obs.content @@ -246,11 +249,13 @@ def test_cmd_run(temp_dir, runtime_cls, run_as_openhands): def test_run_as_user_correct_home_dir(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: obs = _run_cmd_action(runtime, 'cd ~ && pwd') assert obs.exit_code == 0 - if run_as_openhands: + if runtime_cls == LocalRuntime: + assert os.getenv('HOME') in obs.content + elif run_as_openhands: assert '/home/openhands' in obs.content else: assert '/root' in obs.content @@ -259,18 +264,18 @@ def test_run_as_user_correct_home_dir(temp_dir, runtime_cls, run_as_openhands): def test_multi_cmd_run_in_single_line(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: obs = _run_cmd_action(runtime, 'pwd && ls -l') assert obs.exit_code == 0 - assert '/workspace' in obs.content + assert config.workspace_mount_path_in_sandbox in obs.content assert 'total 0' in obs.content finally: _close_test_runtime(runtime) def test_stateful_cmd(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: obs = _run_cmd_action(runtime, 'mkdir -p test') assert obs.exit_code == 0, 'The exit code should be 0.' @@ -280,13 +285,13 @@ def test_stateful_cmd(temp_dir, runtime_cls): obs = _run_cmd_action(runtime, 'pwd') assert obs.exit_code == 0, 'The exit code should be 0.' - assert '/workspace/test' in obs.content + assert f'{config.workspace_mount_path_in_sandbox}/test' in obs.content finally: _close_test_runtime(runtime) def test_failed_cmd(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: obs = _run_cmd_action(runtime, 'non_existing_command') assert obs.exit_code != 0, 'The exit code should not be 0 for a failed command.' @@ -301,9 +306,9 @@ def _create_test_file(host_temp_dir): def test_copy_single_file(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: - sandbox_dir = _get_sandbox_folder(runtime) + sandbox_dir = config.workspace_mount_path_in_sandbox sandbox_file = os.path.join(sandbox_dir, 'test_file.txt') _create_test_file(temp_dir) runtime.copy_to(os.path.join(temp_dir, 'test_file.txt'), sandbox_dir) @@ -331,9 +336,9 @@ def _create_host_test_dir_with_files(test_dir): def test_copy_directory_recursively(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) - sandbox_dir = _get_sandbox_folder(runtime) + sandbox_dir = config.workspace_mount_path_in_sandbox try: temp_dir_copy = os.path.join(temp_dir, 'test_dir') # We need a separate directory, since temp_dir is mounted to /workspace @@ -360,9 +365,9 @@ def test_copy_directory_recursively(temp_dir, runtime_cls): def test_copy_to_non_existent_directory(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: - sandbox_dir = _get_sandbox_folder(runtime) + sandbox_dir = config.workspace_mount_path_in_sandbox _create_test_file(temp_dir) runtime.copy_to( os.path.join(temp_dir, 'test_file.txt'), f'{sandbox_dir}/new_dir' @@ -376,9 +381,9 @@ def test_copy_to_non_existent_directory(temp_dir, runtime_cls): def test_overwrite_existing_file(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: - sandbox_dir = '/workspace' + sandbox_dir = config.workspace_mount_path_in_sandbox obs = _run_cmd_action(runtime, f'ls -alh {sandbox_dir}') assert obs.exit_code == 0 @@ -404,9 +409,9 @@ def test_overwrite_existing_file(temp_dir, runtime_cls): def test_copy_non_existent_file(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) try: - sandbox_dir = _get_sandbox_folder(runtime) + sandbox_dir = config.workspace_mount_path_in_sandbox with pytest.raises(FileNotFoundError): runtime.copy_to( os.path.join(sandbox_dir, 'non_existent_file.txt'), @@ -420,8 +425,8 @@ def test_copy_non_existent_file(temp_dir, runtime_cls): def test_copy_from_directory(temp_dir, runtime_cls): - runtime: Runtime = _load_runtime(temp_dir, runtime_cls) - sandbox_dir = _get_sandbox_folder(runtime) + runtime, config = _load_runtime(temp_dir, runtime_cls) + sandbox_dir = config.workspace_mount_path_in_sandbox try: temp_dir_copy = os.path.join(temp_dir, 'test_dir') # We need a separate directory, since temp_dir is mounted to /workspace @@ -441,22 +446,23 @@ def test_copy_from_directory(temp_dir, runtime_cls): _close_test_runtime(runtime) -def test_git_operation(runtime_cls): +def test_git_operation(temp_dir, runtime_cls): # do not mount workspace, since workspace mount by tests will be owned by root # while the user_id we get via os.getuid() is different from root # which causes permission issues - runtime = _load_runtime( - temp_dir=None, + runtime, config = _load_runtime( + temp_dir=temp_dir, use_workspace=False, runtime_cls=runtime_cls, # Need to use non-root user to expose issues run_as_openhands=True, ) # this will happen if permission of runtime is not properly configured - # fatal: detected dubious ownership in repository at '/workspace' + # fatal: detected dubious ownership in repository at config.workspace_mount_path_in_sandbox try: - obs = _run_cmd_action(runtime, 'sudo chown -R openhands:root .') - assert obs.exit_code == 0 + if runtime_cls != LocalRuntime: + obs = _run_cmd_action(runtime, 'sudo chown -R openhands:root .') + assert obs.exit_code == 0 # check the ownership of the current directory obs = _run_cmd_action(runtime, 'ls -alh .') @@ -464,6 +470,9 @@ def test_git_operation(runtime_cls): # drwx--S--- 2 openhands root 64 Aug 7 23:32 . # drwxr-xr-x 1 root root 4.0K Aug 7 23:33 .. for line in obs.content.split('\n'): + if runtime_cls == LocalRuntime: + continue # skip these checks + if ' ..' in line: # parent directory should be owned by root assert 'root' in line @@ -482,6 +491,19 @@ def test_git_operation(runtime_cls): obs = _run_cmd_action(runtime, 'echo "hello" > test_file.txt') assert obs.exit_code == 0 + if runtime_cls == LocalRuntime: + # set git config author in CI only, not on local machine + logger.info('Setting git config author') + obs = _run_cmd_action( + runtime, + 'git config --file ./.git_config user.name "openhands" && git config --file ./.git_config user.email "openhands@all-hands.dev"', + ) + assert obs.exit_code == 0 + + # Set up git config + obs = _run_cmd_action(runtime, 'git config --file ./.git_config') + assert obs.exit_code == 0 + # git add obs = _run_cmd_action(runtime, 'git add test_file.txt') assert obs.exit_code == 0 @@ -500,7 +522,7 @@ def test_git_operation(runtime_cls): def test_python_version(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: obs = runtime.run_action(CmdRunAction(command='python --version')) @@ -514,7 +536,7 @@ def test_python_version(temp_dir, runtime_cls, run_as_openhands): def test_pwd_property(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Create a subdirectory and verify pwd updates obs = _run_cmd_action(runtime, 'mkdir -p random_dir') @@ -528,7 +550,7 @@ def test_pwd_property(temp_dir, runtime_cls, run_as_openhands): def test_basic_command(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Test simple command obs = _run_cmd_action(runtime, "echo 'hello world'") @@ -556,7 +578,7 @@ def test_basic_command(temp_dir, runtime_cls, run_as_openhands): def test_interactive_command(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime( + runtime, config = _load_runtime( temp_dir, runtime_cls, run_as_openhands, @@ -592,7 +614,7 @@ def test_interactive_command(temp_dir, runtime_cls, run_as_openhands): def test_long_output(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Generate a long output action = CmdRunAction('for i in $(seq 1 5000); do echo "Line $i"; done') @@ -606,7 +628,7 @@ def test_long_output(temp_dir, runtime_cls, run_as_openhands): def test_long_output_exceed_history_limit(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Generate a long output action = CmdRunAction('for i in $(seq 1 50000); do echo "Line $i"; done') @@ -622,7 +644,7 @@ def test_long_output_exceed_history_limit(temp_dir, runtime_cls, run_as_openhand def test_long_output_from_nested_directories(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Create nested directories with many files setup_cmd = 'mkdir -p /tmp/test_dir && cd /tmp/test_dir && for i in $(seq 1 100); do mkdir -p "folder_$i"; for j in $(seq 1 100); do touch "folder_$i/file_$j.txt"; done; done' @@ -647,7 +669,7 @@ def test_long_output_from_nested_directories(temp_dir, runtime_cls, run_as_openh def test_command_backslash(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Create a file with the content "implemented_function" action = CmdRunAction( @@ -674,7 +696,7 @@ def test_command_backslash(temp_dir, runtime_cls, run_as_openhands): def test_command_output_continuation(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Start a command that produces output slowly action = CmdRunAction('for i in {1..5}; do echo $i; sleep 3; done') @@ -714,7 +736,7 @@ def test_command_output_continuation(temp_dir, runtime_cls, run_as_openhands): def test_long_running_command_follow_by_execute( temp_dir, runtime_cls, run_as_openhands ): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Test command that produces output slowly action = CmdRunAction('for i in {1..3}; do echo $i; sleep 3; done') @@ -755,7 +777,7 @@ def test_long_running_command_follow_by_execute( def test_empty_command_errors(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Test empty command without previous command obs = runtime.run_action(CmdRunAction('')) @@ -768,7 +790,7 @@ def test_empty_command_errors(temp_dir, runtime_cls, run_as_openhands): def test_python_interactive_input(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Test Python program that asks for input - properly escaped for bash python_script = """name = input('Enter your name: '); age = input('Enter your age: '); print(f'Hello {name}, you are {age} years old')""" @@ -798,7 +820,7 @@ def test_python_interactive_input(temp_dir, runtime_cls, run_as_openhands): def test_python_interactive_input_without_set_input( temp_dir, runtime_cls, run_as_openhands ): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Test Python program that asks for input - properly escaped for bash python_script = """name = input('Enter your name: '); age = input('Enter your age: '); print(f'Hello {name}, you are {age} years old')""" @@ -837,7 +859,7 @@ def test_python_interactive_input_without_set_input( def test_stress_long_output_with_soft_and_hard_timeout( temp_dir, runtime_cls, run_as_openhands ): - runtime = _load_runtime( + runtime, config = _load_runtime( temp_dir, runtime_cls, run_as_openhands, @@ -925,7 +947,7 @@ def test_stress_long_output_with_soft_and_hard_timeout( def test_bash_remove_prefix(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # create a git repo action = CmdRunAction( diff --git a/tests/runtime/test_browsergym_envs.py b/tests/runtime/test_browsergym_envs.py index 426ecacaf54c..c3806e484ecc 100644 --- a/tests/runtime/test_browsergym_envs.py +++ b/tests/runtime/test_browsergym_envs.py @@ -29,7 +29,7 @@ def has_miniwob(): reason='Requires browsergym-miniwob package to be installed', ) def test_browsergym_eval_env(runtime_cls, temp_dir): - runtime = _load_runtime( + runtime, config = _load_runtime( temp_dir, runtime_cls=runtime_cls, run_as_openhands=False, # need root permission to access file diff --git a/tests/runtime/test_browsing.py b/tests/runtime/test_browsing.py index 0dee3750953f..32b7950796d6 100644 --- a/tests/runtime/test_browsing.py +++ b/tests/runtime/test_browsing.py @@ -17,16 +17,12 @@ # For eval environments, tests need to run with poetry install # ============================================================================================================================ -PY3_FOR_TESTING = '/openhands/micromamba/bin/micromamba run -n openhands python3' - def test_simple_browse(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) # Test browse - action_cmd = CmdRunAction( - command=f'{PY3_FOR_TESTING} -m http.server 8000 > server.log 2>&1 &' - ) + action_cmd = CmdRunAction(command='python3 -m http.server 8000 > server.log 2>&1 &') logger.info(action_cmd, extra={'msg_type': 'ACTION'}) obs = runtime.run_action(action_cmd) logger.info(obs, extra={'msg_type': 'OBSERVATION'}) diff --git a/tests/runtime/test_edit.py b/tests/runtime/test_edit.py index c507166a840d..7039259596c8 100644 --- a/tests/runtime/test_edit.py +++ b/tests/runtime/test_edit.py @@ -28,7 +28,7 @@ def index(): reason='This test requires LLM to run.', ) def test_edit_from_scratch(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: action = FileEditAction( content=ORGINAL, @@ -68,7 +68,7 @@ def index(): reason='This test requires LLM to run.', ) def test_edit(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: action = FileEditAction( content=ORGINAL, @@ -127,7 +127,7 @@ def test_edit(temp_dir, runtime_cls, run_as_openhands): reason='This test requires LLM to run.', ) def test_edit_long_file(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: action = FileEditAction( content=ORIGINAL_LONG, diff --git a/tests/runtime/test_env_vars.py b/tests/runtime/test_env_vars.py index a006e56cfd7d..36fb5fb48716 100644 --- a/tests/runtime/test_env_vars.py +++ b/tests/runtime/test_env_vars.py @@ -15,7 +15,7 @@ def test_env_vars_os_environ(temp_dir, runtime_cls, run_as_openhands): with patch.dict(os.environ, {'SANDBOX_ENV_FOOBAR': 'BAZ'}): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) obs: CmdOutputObservation = runtime.run_action(CmdRunAction(command='env')) print(obs) @@ -33,7 +33,7 @@ def test_env_vars_os_environ(temp_dir, runtime_cls, run_as_openhands): def test_env_vars_runtime_operations(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) # Test adding single env var runtime.add_env_vars({'QUUX': 'abc"def'}) @@ -68,7 +68,7 @@ def test_env_vars_runtime_operations(temp_dir, runtime_cls): def test_env_vars_added_by_config(temp_dir, runtime_cls): - runtime = _load_runtime( + runtime, config = _load_runtime( temp_dir, runtime_cls, runtime_startup_env_vars={'ADDED_ENV_VAR': 'added_value'}, @@ -86,7 +86,7 @@ def test_env_vars_added_by_config(temp_dir, runtime_cls): def test_docker_runtime_env_vars_persist_after_restart(temp_dir): from openhands.runtime.impl.docker.docker_runtime import DockerRuntime - runtime = _load_runtime(temp_dir, DockerRuntime) + runtime, config = _load_runtime(temp_dir, DockerRuntime) # Add a test environment variable runtime.add_env_vars({'GITHUB_TOKEN': 'test_token'}) diff --git a/tests/runtime/test_images.py b/tests/runtime/test_images.py index b7ab82b54b3c..130ea1159336 100644 --- a/tests/runtime/test_images.py +++ b/tests/runtime/test_images.py @@ -18,7 +18,7 @@ def test_bash_python_version(temp_dir, runtime_cls, base_container_image): ]: pytest.skip('This test is only for python-related images') - runtime = _load_runtime( + runtime, config = _load_runtime( temp_dir, runtime_cls, base_container_image=base_container_image ) @@ -52,7 +52,7 @@ def test_nodejs_22_version(temp_dir, runtime_cls, base_container_image): ]: pytest.skip('This test is only for nodejs-related images') - runtime = _load_runtime( + runtime, config = _load_runtime( temp_dir, runtime_cls, base_container_image=base_container_image ) @@ -73,7 +73,7 @@ def test_go_version(temp_dir, runtime_cls, base_container_image): ]: pytest.skip('This test is only for go-related images') - runtime = _load_runtime( + runtime, config = _load_runtime( temp_dir, runtime_cls, base_container_image=base_container_image ) diff --git a/tests/runtime/test_ipython.py b/tests/runtime/test_ipython.py index 11b5db67fe9d..ea0db4ac88b5 100644 --- a/tests/runtime/test_ipython.py +++ b/tests/runtime/test_ipython.py @@ -1,4 +1,4 @@ -"""Test the EventStreamRuntime, which connects to the ActionExecutor running in the sandbox.""" +"""Test the DockerRuntime, which connects to the ActionExecutor running in the sandbox.""" import pytest from conftest import ( @@ -30,7 +30,7 @@ def test_simple_cmd_ipython_and_fileop(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) # Test run command action_cmd = CmdRunAction(command='ls -l') @@ -102,7 +102,7 @@ def test_simple_cmd_ipython_and_fileop(temp_dir, runtime_cls, run_as_openhands): reason='This test is not working in WSL (file ownership)', ) def test_ipython_multi_user(temp_dir, runtime_cls, run_as_openhands): - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) # Test run ipython # get username @@ -174,7 +174,7 @@ def test_ipython_multi_user(temp_dir, runtime_cls, run_as_openhands): def test_ipython_simple(temp_dir, runtime_cls): - runtime = _load_runtime(temp_dir, runtime_cls) + runtime, config = _load_runtime(temp_dir, runtime_cls) # Test run ipython # get username @@ -198,7 +198,7 @@ def test_ipython_simple(temp_dir, runtime_cls): def test_ipython_package_install(temp_dir, runtime_cls, run_as_openhands): """Make sure that cd in bash also update the current working directory in ipython.""" - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) # It should error out since pymsgbox is not installed action = IPythonRunCellAction(code='import pymsgbox') @@ -233,7 +233,7 @@ def test_ipython_package_install(temp_dir, runtime_cls, run_as_openhands): def test_ipython_file_editor_permissions_as_openhands(temp_dir, runtime_cls): """Test file editor permission behavior when running as different users.""" - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands=True) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands=True) # Create a file owned by root with restricted permissions action = CmdRunAction( @@ -313,7 +313,7 @@ def test_ipython_file_editor_permissions_as_openhands(temp_dir, runtime_cls): def test_file_read_and_edit_via_oh_aci(runtime_cls, run_as_openhands): - runtime = _load_runtime(None, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(None, runtime_cls, run_as_openhands) sandbox_dir = '/workspace' actions = [ diff --git a/tests/runtime/test_microagent.py b/tests/runtime/test_microagent.py index 6f0305f6fb96..0f14c2e9b870 100644 --- a/tests/runtime/test_microagent.py +++ b/tests/runtime/test_microagent.py @@ -78,7 +78,7 @@ def test_load_microagents_with_trailing_slashes( """Test loading microagents when directory paths have trailing slashes.""" # Create test files _create_test_microagents(temp_dir) - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Load microagents loaded_agents = runtime.get_microagents_from_selected_repo(None) @@ -119,7 +119,7 @@ def test_load_microagents_with_selected_repo(temp_dir, runtime_cls, run_as_openh repo_dir.mkdir(parents=True) _create_test_microagents(str(repo_dir)) - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Load microagents with selected repository loaded_agents = runtime.get_microagents_from_selected_repo( @@ -174,7 +174,7 @@ def test_load_microagents_with_missing_files(temp_dir, runtime_cls, run_as_openh """ (microagents_dir / 'repo.md').write_text(repo_agent) - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) try: # Load microagents loaded_agents = runtime.get_microagents_from_selected_repo(None) diff --git a/tests/runtime/test_replay.py b/tests/runtime/test_replay.py index 1fc374a38ce5..ca4aeed9d76d 100644 --- a/tests/runtime/test_replay.py +++ b/tests/runtime/test_replay.py @@ -31,9 +31,8 @@ def test_simple_replay(temp_dir, runtime_cls, run_as_openhands): A simple replay test that involves simple terminal operations and edits (creating a simple 2048 game), using the default agent """ - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) - - config = _get_config('basic') + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + config.replay_trajectory_path = './tests/runtime/trajs/basic.json' state: State | None = asyncio.run( run_controller( @@ -59,7 +58,7 @@ def test_simple_gui_replay(temp_dir, runtime_cls, run_as_openhands): 2. In GUI mode, agents typically don't finish; rather, they wait for the next task from the user, so this exported trajectory ends with awaiting_user_input """ - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) config = _get_config('basic_gui_mode') @@ -87,9 +86,8 @@ def test_replay_wrong_initial_state(temp_dir, runtime_cls, run_as_openhands): look like: the following events would still be replayed even though they are meaningless. """ - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) - - config = _get_config('wrong_initial_state') + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + config.replay_trajectory_path = './tests/runtime/trajs/wrong_initial_state.json' state: State | None = asyncio.run( run_controller( @@ -120,7 +118,7 @@ def test_replay_basic_interactions(temp_dir, runtime_cls, run_as_openhands): interference (no asking for user input). 2) The user messages in the trajectory should appear in the history. """ - runtime = _load_runtime(temp_dir, runtime_cls, run_as_openhands) + runtime, config = _load_runtime(temp_dir, runtime_cls, run_as_openhands) config = _get_config('basic_interactions') diff --git a/tests/runtime/test_stress_docker_runtime.py b/tests/runtime/test_stress_docker_runtime.py index 6e8a9d5957e8..b679a0836253 100644 --- a/tests/runtime/test_stress_docker_runtime.py +++ b/tests/runtime/test_stress_docker_runtime.py @@ -7,7 +7,7 @@ def test_stress_docker_runtime(temp_dir, runtime_cls, repeat=1): - runtime = _load_runtime( + runtime, config = _load_runtime( temp_dir, runtime_cls, docker_runtime_kwargs={