mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 23:08:04 -05:00
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:
2
.github/workflows/ghcr-build.yml
vendored
2
.github/workflows/ghcr-build.yml
vendored
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
342
openhands/runtime/impl/local/local_runtime.py
Normal file
342
openhands/runtime/impl/local/local_runtime.py
Normal 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
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
]
|
||||
|
||||
|
||||
@@ -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
141
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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 = "*"
|
||||
|
||||
@@ -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',
|
||||
]
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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'})
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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')
|
||||
|
||||
|
||||
@@ -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={
|
||||
|
||||
Reference in New Issue
Block a user