feat: Add LocalRuntime (#5284)

Co-authored-by: openhands <openhands@all-hands.dev>
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
This commit is contained in:
Xingyao Wang
2025-02-07 11:35:14 -05:00
committed by GitHub
parent ce82545437
commit 478b225d11
23 changed files with 661 additions and 145 deletions

View File

@@ -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

View File

@@ -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:

View File

@@ -3,6 +3,7 @@ from openhands.runtime.impl.docker.docker_runtime import (
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')

View File

@@ -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 @@ class ActionExecutor:
)
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 @@ class ActionExecutor:
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:

View File

@@ -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

View File

@@ -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,
)

View File

@@ -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)

View File

@@ -184,9 +184,10 @@ class BashSession:
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(

View File

@@ -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,
]

View File

@@ -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}')

141
poetry.lock generated
View File

@@ -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"

View File

@@ -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 = "*"

View File

@@ -3,16 +3,16 @@ import random
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',
]

View File

@@ -7,14 +7,13 @@ from pathlib import Path
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 @@ done && echo "created files"
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 @@ done && echo "success"
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 @@ EOF""")
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(

View File

@@ -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

View File

@@ -17,16 +17,12 @@ from openhands.events.observation import (
# 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'})

View File

@@ -28,7 +28,7 @@ if __name__ == '__main__':
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 @@ This is line 101 + 10
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,

View File

@@ -15,7 +15,7 @@ from openhands.events.observation import CmdOutputObservation
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'})

View File

@@ -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
)

View File

@@ -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 @@ from openhands.events.observation import (
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 @@ print(file_editor(command='undo_edit', path='/workspace/test.txt'))
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 = [

View File

@@ -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 @@ Repository-specific test instructions.
"""
(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)

View File

@@ -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')

View File

@@ -7,7 +7,7 @@ from openhands.events.action import CmdRunAction
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={