diff --git a/docs/modules/usage/installation.mdx b/docs/modules/usage/installation.mdx index 11f8640bbd..56f2465d9a 100644 --- a/docs/modules/usage/installation.mdx +++ b/docs/modules/usage/installation.mdx @@ -5,6 +5,7 @@ - MacOS with [Docker Desktop support](https://docs.docker.com/desktop/setup/install/mac-install/#system-requirements) - Linux - Windows with [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) and [Docker Desktop support](https://docs.docker.com/desktop/setup/install/windows-install/#system-requirements) +- Windows without WSL (see [Windows Without WSL Guide](./windows-without-wsl)) A system with a modern processor and a minimum of **4GB RAM** is recommended to run OpenHands. @@ -51,6 +52,10 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to The docker command below to start the app must be run inside the WSL terminal. ::: + **Alternative: Windows without WSL** + + If you prefer to run OpenHands on Windows without WSL or Docker, see our [Windows Without WSL Guide](./windows-without-wsl). + ## Start the App diff --git a/docs/modules/usage/windows-without-wsl.md b/docs/modules/usage/windows-without-wsl.md new file mode 100644 index 0000000000..805b08ed59 --- /dev/null +++ b/docs/modules/usage/windows-without-wsl.md @@ -0,0 +1,195 @@ +# Running OpenHands GUI on Windows Without WSL + +This guide provides step-by-step instructions for running OpenHands on a Windows machine without using WSL or Docker. + +## Prerequisites + +1. **Windows 10/11** - A modern Windows operating system +2. **PowerShell 7+** - While Windows PowerShell comes pre-installed on Windows 10/11, PowerShell 7+ is strongly recommended to avoid compatibility issues (see Troubleshooting section for "System.Management.Automation" errors) +3. **.NET Core Runtime** - Required for the PowerShell integration via pythonnet +4. **Python 3.12 or 3.13** - Python 3.12 or 3.13 is required (Python 3.14 is not supported due to pythonnet compatibility) +5. **Git** - For cloning the repository and version control +6. **Node.js and npm** - For running the frontend + +## Step 1: Install Required Software + +1. **Install Python 3.12 or 3.13** + - Download Python 3.12.x or 3.13.x from [python.org](https://www.python.org/downloads/) + - During installation, check "Add Python to PATH" + - Verify installation by opening PowerShell and running: + ```powershell + python --version + ``` + +2. **Install PowerShell 7** + - Download and install PowerShell 7 from the [official PowerShell GitHub repository](https://github.com/PowerShell/PowerShell/releases) + - Choose the MSI installer appropriate for your system (x64 for most modern computers) + - Run the installer with default options + - Verify installation by opening a new terminal and running: + ```powershell + pwsh --version + ``` + - Using PowerShell 7 (pwsh) instead of Windows PowerShell will help avoid "System.Management.Automation" errors + +3. **Install .NET Core Runtime** + - Download and install the .NET Core Runtime from [Microsoft's .NET download page](https://dotnet.microsoft.com/download) + - Choose the latest .NET Core Runtime (not SDK) + - Verify installation by opening PowerShell and running: + ```powershell + dotnet --info + ``` + - This step is required for the PowerShell integration via pythonnet. Without it, OpenHands will fall back to a more limited PowerShell implementation. + +4. **Install Git** + - Download Git from [git-scm.com](https://git-scm.com/download/win) + - Use default installation options + - Verify installation: + ```powershell + git --version + ``` + +5. **Install Node.js and npm** + - Download Node.js from [nodejs.org](https://nodejs.org/) (LTS version recommended) + - During installation, accept the default options which will install npm as well + - Verify installation: + ```powershell + node --version + npm --version + ``` + +6. **Install Poetry** + - Open PowerShell as Administrator and run: + ```powershell + (Invoke-WebRequest -Uri https://install.python-poetry.org -UseBasicParsing).Content | python - + ``` + - Add Poetry to your PATH: + ```powershell + $env:Path += ";$env:APPDATA\Python\Scripts" + ``` + - Verify installation: + ```powershell + poetry --version + ``` + +## Step 2: Clone and Set Up OpenHands + +1. **Clone the Repository** + ```powershell + git clone https://github.com/All-Hands-AI/OpenHands.git + cd OpenHands + ``` + +2. **Install Dependencies** + ```powershell + poetry install + ``` + + This will install all required dependencies, including: + - pythonnet - Required for Windows PowerShell integration + - All other OpenHands dependencies + +## Step 3: Run OpenHands + +1. **Build the Frontend** + ```powershell + cd frontend + npm install + npm run build + cd .. + ``` + + This will build the frontend files that the backend will serve. + +2. **Start the Backend** + ```powershell + # Make sure to use PowerShell 7 (pwsh) instead of Windows PowerShell + pwsh + $env:RUNTIME="local"; poetry run uvicorn openhands.server.listen:app --host 0.0.0.0 --port 3000 --reload --reload-exclude "./workspace" + ``` + + This will start the OpenHands app using the local runtime with PowerShell integration, available at `localhost:3000`. + + > **Note**: If you encounter a `RuntimeError: Directory './frontend/build' does not exist` error, make sure you've built the frontend first using the command above. + + > **Important**: Using PowerShell 7 (pwsh) instead of Windows PowerShell is recommended to avoid "System.Management.Automation" errors. If you encounter this error, see the Troubleshooting section below. + +3. **Alternatively, Run the Frontend in Development Mode (in a separate PowerShell window)** + ```powershell + cd frontend + npm run dev + ``` + +4. **Access the OpenHands GUI** + + Open your browser and navigate to: + ``` + http://localhost:3000 + ``` + + > **Note**: If you're running the frontend in development mode (using `npm run dev`), use port 3001 instead: `http://localhost:3001` + +## Limitations on Windows + +When running OpenHands on Windows without WSL or Docker, be aware of the following limitations: + +1. **Browser Tool Not Supported**: The browser tool is not currently supported on Windows. + +2. **.NET Core Requirement**: The PowerShell integration requires .NET Core Runtime to be installed. If .NET Core is not available, OpenHands will automatically fall back to a more limited PowerShell implementation with reduced functionality. + +3. **Interactive Shell Commands**: Some interactive shell commands may not work as expected. The PowerShell session implementation has limitations compared to the bash session used on Linux/macOS. + +4. **Path Handling**: Windows uses backslashes (`\`) in paths, which may require adjustments when working with code examples designed for Unix-like systems. + +## Troubleshooting + +### "System.Management.Automation" Not Found Error + +If you encounter an error message stating that "System.Management.Automation" was not found, this typically indicates that you have a minimal version of PowerShell installed or that the .NET components required for PowerShell integration are missing. + +> **IMPORTANT**: This error is most commonly caused by using the built-in Windows PowerShell (powershell.exe) instead of PowerShell 7 (pwsh.exe). Even if you installed PowerShell 7 during the prerequisites, you may still be using the older Windows PowerShell by default. + +To resolve this issue: + +1. **Install the latest version of PowerShell 7** from the official Microsoft repository: + - Visit [https://github.com/PowerShell/PowerShell/releases](https://github.com/PowerShell/PowerShell/releases) + - Download and install the latest MSI package for your system architecture (x64 for most systems) + - During installation, ensure you select the following options: + - "Add PowerShell to PATH environment variable" + - "Register Windows PowerShell 7 as the default shell" + - "Enable PowerShell remoting" + - The installer will place PowerShell 7 in `C:\Program Files\PowerShell\7` by default + +2. **Restart your terminal or command prompt** to ensure the new PowerShell is available + +3. **Verify the installation** by running: + ```powershell + pwsh --version + ``` + + You should see output indicating PowerShell 7.x.x + +4. **Run OpenHands using PowerShell 7** instead of Windows PowerShell: + ```powershell + pwsh + cd path\to\openhands + $env:RUNTIME="local"; poetry run uvicorn openhands.server.listen:app --host 0.0.0.0 --port 3000 --reload --reload-exclude "./workspace" + ``` + + > **Note**: Make sure you're explicitly using `pwsh` (PowerShell 7) and not `powershell` (Windows PowerShell). The command prompt or terminal title should say "PowerShell 7" rather than just "Windows PowerShell". + +5. **If the issue persists**, ensure that you have the .NET Runtime installed: + - Download and install the latest .NET Runtime from [Microsoft's .NET download page](https://dotnet.microsoft.com/download) + - Choose ".NET Runtime" (not SDK) version 6.0 or later + - After installation, verify it's properly installed by running: + ```powershell + dotnet --info + ``` + - Restart your computer after installation + - Try running OpenHands again + +6. **Ensure that the .NET Framework is properly installed** on your system: + - Go to Control Panel > Programs > Programs and Features > Turn Windows features on or off + - Make sure ".NET Framework 4.8 Advanced Services" is enabled + - Click OK and restart if prompted + +This error occurs because OpenHands uses the pythonnet package to interact with PowerShell, which requires the System.Management.Automation assembly from the .NET framework. A minimal PowerShell installation or older Windows PowerShell (rather than PowerShell 7+) might not include all the necessary components for this integration. diff --git a/openhands/mcp/utils.py b/openhands/mcp/utils.py index 7c7a1cf238..fb932edee3 100644 --- a/openhands/mcp/utils.py +++ b/openhands/mcp/utils.py @@ -50,6 +50,15 @@ def convert_mcp_clients_to_tools(mcp_clients: list[MCPClient] | None) -> list[di async def create_mcp_clients( sse_servers: list[MCPSSEServerConfig], conversation_id: str | None = None ) -> list[MCPClient]: + import sys + + # Skip MCP clients on Windows + if sys.platform == 'win32': + logger.info( + 'MCP functionality is disabled on Windows, skipping client creation' + ) + return [] + mcp_clients: list[MCPClient] = [] # Initialize SSE connections if sse_servers: @@ -89,6 +98,13 @@ async def fetch_mcp_tools_from_config(mcp_config: MCPConfig) -> list[dict]: Returns: A list of tool dictionaries. Returns an empty list if no connections could be established. """ + import sys + + # Skip MCP tools on Windows + if sys.platform == 'win32': + logger.info('MCP functionality is disabled on Windows, skipping tool fetching') + return [] + mcp_clients = [] mcp_tools = [] try: @@ -131,6 +147,15 @@ async def call_tool_mcp(mcp_clients: list[MCPClient], action: MCPAction) -> Obse Returns: The observation from the MCP server """ + import sys + + from openhands.events.observation import ErrorObservation + + # Skip MCP tools on Windows + if sys.platform == 'win32': + logger.info('MCP functionality is disabled on Windows') + return ErrorObservation('MCP functionality is not available on Windows') + if not mcp_clients: raise ValueError('No MCP clients found') @@ -169,6 +194,13 @@ async def add_mcp_tools_to_agent( """ Add MCP tools to an agent. """ + import sys + + # Skip MCP tools on Windows + if sys.platform == 'win32': + logger.info('MCP functionality is disabled on Windows, skipping MCP tools') + agent.set_mcp_tools([]) + return assert runtime.runtime_initialized, ( 'Runtime must be initialized before adding MCP tools' diff --git a/openhands/runtime/action_execution_server.py b/openhands/runtime/action_execution_server.py index 30a48df1a5..113aff9d77 100644 --- a/openhands/runtime/action_execution_server.py +++ b/openhands/runtime/action_execution_server.py @@ -677,44 +677,53 @@ if __name__ == '__main__': await client.ainit() logger.info('ActionExecutor initialized.') - # Initialize and mount MCP Router - logger.info('Initializing MCP Router...') - mcp_router = MCPRouter( - profile_path=MCP_ROUTER_PROFILE_PATH, - router_config=RouterConfig( - api_key=SESSION_API_KEY, - auth_enabled=bool(SESSION_API_KEY), - ), - ) - allowed_origins = ['*'] - sse_app = await mcp_router.get_sse_server_app( - allow_origins=allowed_origins, include_lifespan=False - ) + # Check if we're on Windows + is_windows = sys.platform == 'win32' - # Check for route conflicts before mounting - main_app_routes = {route.path for route in app.routes} - sse_app_routes = {route.path for route in sse_app.routes} - conflicting_routes = main_app_routes.intersection(sse_app_routes) - - if conflicting_routes: - logger.error(f'Route conflicts detected: {conflicting_routes}') - raise RuntimeError( - f'Cannot mount SSE app - conflicting routes found: {conflicting_routes}' + # Initialize and mount MCP Router (skip on Windows) + if is_windows: + logger.info('Skipping MCP Router initialization on Windows') + mcp_router = None + else: + logger.info('Initializing MCP Router...') + mcp_router = MCPRouter( + profile_path=MCP_ROUTER_PROFILE_PATH, + router_config=RouterConfig( + api_key=SESSION_API_KEY, + auth_enabled=bool(SESSION_API_KEY), + ), + ) + allowed_origins = ['*'] + sse_app = await mcp_router.get_sse_server_app( + allow_origins=allowed_origins, include_lifespan=False ) - app.mount('/', sse_app) - logger.info( - f'Mounted MCP Router SSE app at root path with allowed origins: {allowed_origins}' - ) + # Only mount SSE app if MCP Router is initialized (not on Windows) + if mcp_router is not None: + # Check for route conflicts before mounting + main_app_routes = {route.path for route in app.routes} + sse_app_routes = {route.path for route in sse_app.routes} + conflicting_routes = main_app_routes.intersection(sse_app_routes) - # Additional debug logging - if logger.isEnabledFor(logging.DEBUG): - logger.debug('Main app routes:') - for route in main_app_routes: - logger.debug(f' {route}') - logger.debug('MCP SSE server app routes:') - for route in sse_app_routes: - logger.debug(f' {route}') + if conflicting_routes: + logger.error(f'Route conflicts detected: {conflicting_routes}') + raise RuntimeError( + f'Cannot mount SSE app - conflicting routes found: {conflicting_routes}' + ) + + app.mount('/', sse_app) + logger.info( + f'Mounted MCP Router SSE app at root path with allowed origins: {allowed_origins}' + ) + + # Additional debug logging + if logger.isEnabledFor(logging.DEBUG): + logger.debug('Main app routes:') + for route in main_app_routes: + logger.debug(f' {route}') + logger.debug('MCP SSE server app routes:') + for route in sse_app_routes: + logger.debug(f' {route}') yield @@ -816,6 +825,23 @@ if __name__ == '__main__': @app.post('/update_mcp_server') async def update_mcp_server(request: Request): + # Check if we're on Windows + is_windows = sys.platform == 'win32' + + if is_windows: + # On Windows, just return a success response without doing anything + logger.info( + 'MCP server update request received on Windows - skipping as MCP is disabled' + ) + return JSONResponse( + status_code=200, + content={ + 'detail': 'MCP server update skipped (MCP is disabled on Windows)', + 'router_error_log': '', + }, + ) + + # Non-Windows implementation assert mcp_router is not None assert os.path.exists(MCP_ROUTER_PROFILE_PATH) diff --git a/openhands/runtime/base.py b/openhands/runtime/base.py index 70135cacd3..9325bc6d3b 100644 --- a/openhands/runtime/base.py +++ b/openhands/runtime/base.py @@ -218,35 +218,66 @@ class Runtime(FileEditRuntimeMixin): # Note: we don't log the vars values, they're leaking info logger.debug('Added env vars to IPython') - # Add env vars to the Bash shell and .bashrc for persistence - cmd = '' - bashrc_cmd = '' - for key, value in env_vars.items(): - # Note: json.dumps gives us nice escaping for free - cmd += f'export {key}={json.dumps(value)}; ' - # Add to .bashrc if not already present - bashrc_cmd += f'grep -q "^export {key}=" ~/.bashrc || echo "export {key}={json.dumps(value)}" >> ~/.bashrc; ' - if not cmd: - return - cmd = cmd.strip() - logger.debug( - 'Adding env vars to bash' - ) # don't log the vars values, they're leaking info + # Check if we're on Windows + import os + import sys - obs = self.run(CmdRunAction(cmd)) - if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0: - raise RuntimeError( - f'Failed to add env vars [{env_vars.keys()}] to environment: {obs.content}' - ) + is_windows = os.name == 'nt' or sys.platform == 'win32' - # Add to .bashrc for persistence - bashrc_cmd = bashrc_cmd.strip() - logger.debug(f'Adding env var to .bashrc: {env_vars.keys()}') - obs = self.run(CmdRunAction(bashrc_cmd)) - if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0: - raise RuntimeError( - f'Failed to add env vars [{env_vars.keys()}] to .bashrc: {obs.content}' - ) + if is_windows: + # Add env vars using PowerShell commands for Windows + cmd = '' + for key, value in env_vars.items(): + # Use PowerShell's $env: syntax for environment variables + # Note: json.dumps gives us nice escaping for free + cmd += f'$env:{key} = {json.dumps(value)}; ' + + if not cmd: + return + + cmd = cmd.strip() + logger.debug('Adding env vars to PowerShell') # don't log the values + + obs = self.run(CmdRunAction(cmd)) + if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0: + raise RuntimeError( + f'Failed to add env vars [{env_vars.keys()}] to environment: {obs.content}' + ) + + # We don't add to profile persistence on Windows as it's more complex + # and varies between PowerShell versions + logger.debug(f'Added env vars to PowerShell session: {env_vars.keys()}') + + else: + # Original bash implementation for Unix systems + cmd = '' + bashrc_cmd = '' + for key, value in env_vars.items(): + # Note: json.dumps gives us nice escaping for free + cmd += f'export {key}={json.dumps(value)}; ' + # Add to .bashrc if not already present + bashrc_cmd += f'grep -q "^export {key}=" ~/.bashrc || echo "export {key}={json.dumps(value)}" >> ~/.bashrc; ' + + if not cmd: + return + + cmd = cmd.strip() + logger.debug('Adding env vars to bash') # don't log the values + + obs = self.run(CmdRunAction(cmd)) + if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0: + raise RuntimeError( + f'Failed to add env vars [{env_vars.keys()}] to environment: {obs.content}' + ) + + # Add to .bashrc for persistence + bashrc_cmd = bashrc_cmd.strip() + logger.debug(f'Adding env var to .bashrc: {env_vars.keys()}') + obs = self.run(CmdRunAction(bashrc_cmd)) + if not isinstance(obs, CmdOutputObservation) or obs.exit_code != 0: + raise RuntimeError( + f'Failed to add env vars [{env_vars.keys()}] to .bashrc: {obs.content}' + ) def on_event(self, event: Event) -> None: if isinstance(event, Action): @@ -406,9 +437,13 @@ class Runtime(FileEditRuntimeMixin): else f'git checkout -b {openhands_workspace_branch}' ) - action = CmdRunAction( - command=f'{clone_command} ; cd {dir_name} ; {checkout_command}', + clone_action = CmdRunAction(command=clone_command) + self.run_action(clone_action) + + cd_checkout_action = CmdRunAction( + command=f'cd {dir_name} && {checkout_command}' ) + action = cd_checkout_action self.log('info', f'Cloning repo: {selected_repository}') self.run_action(action) return dir_name @@ -649,16 +684,13 @@ fi # Get authenticated URL and do a shallow clone (--depth 1) for efficiency remote_url = self._get_authenticated_git_url(org_openhands_repo) - clone_cmd = f"git clone --depth 1 {remote_url} {org_repo_dir} 2>/dev/null || echo 'Org repo not found'" + + clone_cmd = f'git clone --depth 1 {remote_url} {org_repo_dir}' action = CmdRunAction(command=clone_cmd) obs = self.run_action(action) - if ( - isinstance(obs, CmdOutputObservation) - and obs.exit_code == 0 - and 'Org repo not found' not in obs.content - ): + if isinstance(obs, CmdOutputObservation) and obs.exit_code == 0: self.log( 'info', f'Successfully cloned org-level microagents from {org_openhands_repo}', diff --git a/openhands/runtime/impl/action_execution/action_execution_client.py b/openhands/runtime/impl/action_execution/action_execution_client.py index b35e0be92e..dd13d24d3b 100644 --- a/openhands/runtime/impl/action_execution/action_execution_client.py +++ b/openhands/runtime/impl/action_execution/action_execution_client.py @@ -354,6 +354,14 @@ class ActionExecutionClient(Runtime): def get_mcp_config( self, extra_stdio_servers: list[MCPStdioServerConfig] | None = None ) -> MCPConfig: + import sys + + # Check if we're on Windows - MCP is disabled on Windows + if sys.platform == 'win32': + # Return empty MCP config on Windows + self.log('debug', 'MCP is disabled on Windows, returning empty config') + return MCPConfig(sse_servers=[], stdio_servers=[]) + # Add the runtime as another MCP server updated_mcp_config = self.config.mcp.model_copy() @@ -436,6 +444,15 @@ class ActionExecutionClient(Runtime): return updated_mcp_config async def call_tool_mcp(self, action: MCPAction) -> Observation: + import sys + + from openhands.events.observation import ErrorObservation + + # Check if we're on Windows - MCP is disabled on Windows + if sys.platform == 'win32': + self.log('info', 'MCP functionality is disabled on Windows') + return ErrorObservation('MCP functionality is not available on Windows') + # Import here to avoid circular imports from openhands.mcp.utils import call_tool_mcp as call_tool_mcp_handler from openhands.mcp.utils import create_mcp_clients diff --git a/openhands/runtime/impl/local/local_runtime.py b/openhands/runtime/impl/local/local_runtime.py index 0d50270bae..1be1acb7b1 100644 --- a/openhands/runtime/impl/local/local_runtime.py +++ b/openhands/runtime/impl/local/local_runtime.py @@ -238,6 +238,7 @@ class LocalRuntime(ActionExecutionClient): env['PYTHONPATH'] = os.pathsep.join([code_repo_path, env.get('PYTHONPATH', '')]) env['OPENHANDS_REPO_PATH'] = code_repo_path env['LOCAL_RUNTIME_MODE'] = '1' + env['VSCODE_PORT'] = str(self._vscode_port) # Derive environment paths using sys.executable interpreter_path = sys.executable diff --git a/openhands/runtime/plugins/vscode/__init__.py b/openhands/runtime/plugins/vscode/__init__.py index e583e2a107..6dd80c1629 100644 --- a/openhands/runtime/plugins/vscode/__init__.py +++ b/openhands/runtime/plugins/vscode/__init__.py @@ -1,6 +1,7 @@ import asyncio import os import shutil +import sys import uuid from dataclasses import dataclass from pathlib import Path @@ -26,6 +27,15 @@ class VSCodePlugin(Plugin): gateway_process: asyncio.subprocess.Process async def initialize(self, username: str) -> None: + # Check if we're on Windows - VSCode plugin is not supported on Windows + if os.name == 'nt' or sys.platform == 'win32': + self.vscode_port = None + self.vscode_connection_token = None + logger.warning( + 'VSCode plugin is not supported on Windows. Plugin will be disabled.' + ) + return + if username not in ['root', 'openhands']: self.vscode_port = None self.vscode_connection_token = None @@ -38,9 +48,20 @@ class VSCodePlugin(Plugin): # Set up VSCode settings.json self._setup_vscode_settings() - self.vscode_port = int(os.environ['VSCODE_PORT']) + try: + self.vscode_port = int(os.environ['VSCODE_PORT']) + except (KeyError, ValueError): + logger.warning( + 'VSCODE_PORT environment variable not set or invalid. VSCode plugin will be disabled.' + ) + return + self.vscode_connection_token = str(uuid.uuid4()) - assert check_port_available(self.vscode_port) + if not check_port_available(self.vscode_port): + logger.warning( + f'Port {self.vscode_port} is not available. VSCode plugin will be disabled.' + ) + return cmd = ( f"su - {username} -s /bin/bash << 'EOF'\n" f'sudo chown -R {username}:{username} /openhands/.openvscode-server\n' diff --git a/openhands/utils/chunk_localizer.py b/openhands/utils/chunk_localizer.py index 8b2e986c14..9f1437dd3d 100644 --- a/openhands/utils/chunk_localizer.py +++ b/openhands/utils/chunk_localizer.py @@ -4,8 +4,8 @@ This is primarily used to localize the most relevant chunks in a file for a given query (e.g. edit draft produced by the agent). """ -import pylcs from pydantic import BaseModel +from rapidfuzz.distance import LCSseq from tree_sitter_languages import get_parser from openhands.core.logger import openhands_logger as logger @@ -65,7 +65,9 @@ def normalized_lcs(chunk: str, query: str) -> float: """ if len(chunk) == 0: return 0.0 - _score = pylcs.lcs_sequence_length(chunk, query) + + _score = LCSseq.similarity(chunk, query) + return _score / len(chunk) diff --git a/poetry.lock b/poetry.lock index eb01eea879..457a534b43 100644 --- a/poetry.lock +++ b/poetry.lock @@ -3011,7 +3011,7 @@ grpcio = {version = ">=1.49.1,<2.0dev", optional = true, markers = "python_versi grpcio-status = {version = ">=1.49.1,<2.0.dev0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = [ {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" requests = ">=2.18.0,<3.0.0" @@ -3230,7 +3230,7 @@ google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" grpc-google-iam-v1 = ">=0.14.0,<1.0.0" proto-plus = [ {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, - {version = ">=1.22.3,<2.0.0"}, + {version = ">=1.22.3,<2.0.0", markers = "python_version < \"3.13\""}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -5403,11 +5403,8 @@ files = [ {file = "lxml-5.4.0-cp36-cp36m-win_amd64.whl", hash = "sha256:7ce1a171ec325192c6a636b64c94418e71a1964f56d002cc28122fceff0b6121"}, {file = "lxml-5.4.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:795f61bcaf8770e1b37eec24edf9771b307df3af74d1d6f27d812e15a9ff3872"}, {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:29f451a4b614a7b5b6c2e043d7b64a15bd8304d7e767055e8ab68387a8cacf4e"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:891f7f991a68d20c75cb13c5c9142b2a3f9eb161f1f12a9489c82172d1f133c0"}, {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4aa412a82e460571fad592d0f93ce9935a20090029ba08eca05c614f99b0cc92"}, - {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_aarch64.whl", hash = "sha256:ac7ba71f9561cd7d7b55e1ea5511543c0282e2b6450f122672a2694621d63b7e"}, {file = "lxml-5.4.0-cp37-cp37m-manylinux_2_28_x86_64.whl", hash = "sha256:c5d32f5284012deaccd37da1e2cd42f081feaa76981f0eaa474351b68df813c5"}, - {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:ce31158630a6ac85bddd6b830cffd46085ff90498b397bd0a259f59d27a12188"}, {file = "lxml-5.4.0-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:31e63621e073e04697c1b2d23fcb89991790eef370ec37ce4d5d469f40924ed6"}, {file = "lxml-5.4.0-cp37-cp37m-win32.whl", hash = "sha256:be2ba4c3c5b7900246a8f866580700ef0d538f2ca32535e991027bdaba944063"}, {file = "lxml-5.4.0-cp37-cp37m-win_amd64.whl", hash = "sha256:09846782b1ef650b321484ad429217f5154da4d6e786636c38e434fa32e94e49"}, @@ -7593,21 +7590,6 @@ files = [ [package.dependencies] pyasn1 = ">=0.6.1,<0.7.0" -[[package]] -name = "pybind11" -version = "2.13.6" -description = "Seamless operability between C++11 and Python" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "pybind11-2.13.6-py3-none-any.whl", hash = "sha256:237c41e29157b962835d356b370ededd57594a26d5894a795960f0047cb5caf5"}, - {file = "pybind11-2.13.6.tar.gz", hash = "sha256:ba6af10348c12b24e92fa086b39cfba0eff619b61ac77c406167d813b096d39a"}, -] - -[package.extras] -global = ["pybind11-global (==2.13.6)"] - [[package]] name = "pycodestyle" version = "2.13.0" @@ -7959,27 +7941,6 @@ files = [ {file = "pylatexenc-2.10.tar.gz", hash = "sha256:3dd8fd84eb46dc30bee1e23eaab8d8fb5a7f507347b23e5f38ad9675c84f40d3"}, ] -[[package]] -name = "pylcs" -version = "0.1.1" -description = "super fast cpp implementation of longest common subsequence" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pylcs-0.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:7b8adea6b41dff27332c967533ec3c42a5e94171be778d6f01f0c5cee82e7604"}, - {file = "pylcs-0.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:9ff06e037c54056cb67d6ef5ad946c0360afeff7d43be67ce09e55201ecc15cc"}, - {file = "pylcs-0.1.1-cp35-cp35m-win_amd64.whl", hash = "sha256:d2ebf340aa180d841939d9ec1168dfd072992dda1d48148ceb07b65b1ab62ffa"}, - {file = "pylcs-0.1.1-cp36-cp36m-win_amd64.whl", hash = "sha256:b6c43b63e20048f8fec7e122fbc08c238940a0ee5302bf84a70db22c7f8cc836"}, - {file = "pylcs-0.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:db52d55cfdf813af974bcc164aedbd29274da83086877bf05778aa7fbf777f7f"}, - {file = "pylcs-0.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:954495f1c164ccb722b835e7028783f8a38d85ed5f6ff7b9d50143896c6cff9b"}, - {file = "pylcs-0.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:0f4c82fad8c0429abef9e98fb98904459c4f5f9fb9b6ce20e0df0841a6a48a54"}, - {file = "pylcs-0.1.1.tar.gz", hash = "sha256:632c69235d77cda0ba524d82796878801d2f46131fc59e730c98767fc4ce1307"}, -] - -[package.dependencies] -pybind11 = ">=2.2" - [[package]] name = "pynacl" version = "1.5.0" @@ -12193,4 +12154,4 @@ cffi = ["cffi (>=1.11)"] [metadata] lock-version = "2.1" python-versions = "^3.12,<3.14" -content-hash = "509493c2d53d5923054d7521079a212c6e67323389b75886b6e1dccc9869127c" +content-hash = "071b19070a00bfbae9cab2332a58ce9e0f31fcf594bc4ad32fd647783611396c" diff --git a/pyproject.toml b/pyproject.toml index e880b75b66..9fa939087e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,12 +20,12 @@ packages = [ [tool.poetry.dependencies] python = "^3.12,<3.14" -litellm = "^1.60.0, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272) -aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13 -google-generativeai = "*" # To use litellm with Gemini Pro API -google-api-python-client = "^2.164.0" # For Google Sheets API -google-auth-httplib2 = "*" # For Google Sheets authentication -google-auth-oauthlib = "*" # For Google Sheets OAuth +litellm = "^1.60.0, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272) +aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13 +google-generativeai = "*" # To use litellm with Gemini Pro API +google-api-python-client = "^2.164.0" # For Google Sheets API +google-auth-httplib2 = "*" # For Google Sheets authentication +google-auth-oauthlib = "*" # For Google Sheets OAuth termcolor = "*" docker = "*" fastapi = "*" @@ -34,7 +34,7 @@ uvicorn = "*" types-toml = "*" numpy = "*" json-repair = "*" -browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface +browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface html2text = "*" e2b = ">=1.0.5,<1.4.0" pexpect = "*" @@ -58,9 +58,9 @@ python-pptx = "*" pylatexenc = "*" tornado = "*" python-dotenv = "*" -pylcs = "^0.1.1" +rapidfuzz = "^3.9.0" whatthepatch = "^1.0.6" -protobuf = "^4.21.6,<5.0.0" # chromadb currently fails on 5.0+ +protobuf = "^4.21.6,<5.0.0" # chromadb currently fails on 5.0+ opentelemetry-api = "1.25.0" opentelemetry-exporter-otlp-proto-grpc = "1.25.0" modal = ">=0.66.26,<0.78.0" diff --git a/tests/unit/test_runtime_git_tokens.py b/tests/unit/test_runtime_git_tokens.py index d88fe1712b..fb880d9aec 100644 --- a/tests/unit/test_runtime_git_tokens.py +++ b/tests/unit/test_runtime_git_tokens.py @@ -327,8 +327,22 @@ async def test_clone_or_init_repo_github_with_token(temp_dir, monkeypatch): result = await runtime.clone_or_init_repo(git_provider_tokens, 'owner/repo', None) - cmd = runtime.run_action_calls[0].command - assert f'git clone https://{github_token}@github.com/owner/repo.git repo' in cmd + # Verify that git clone and checkout were called as separate commands + assert len(runtime.run_action_calls) == 2 + assert isinstance(runtime.run_action_calls[0], CmdRunAction) + assert isinstance(runtime.run_action_calls[1], CmdRunAction) + + # Check that the first command is the git clone with the correct URL format with token + clone_cmd = runtime.run_action_calls[0].command + assert ( + f'git clone https://{github_token}@github.com/owner/repo.git repo' in clone_cmd + ) + + # Check that the second command is the checkout + checkout_cmd = runtime.run_action_calls[1].command + assert 'cd repo' in checkout_cmd + assert 'git checkout -b openhands-workspace-' in checkout_cmd + assert result == 'repo' @@ -346,15 +360,20 @@ async def test_clone_or_init_repo_github_no_token(temp_dir, monkeypatch): mock_repo_and_patch(monkeypatch, provider=ProviderType.GITHUB) result = await runtime.clone_or_init_repo(None, 'owner/repo', None) - # Verify that git clone was called with the public URL - assert len(runtime.run_action_calls) == 1 + # Verify that git clone and checkout were called as separate commands + assert len(runtime.run_action_calls) == 2 assert isinstance(runtime.run_action_calls[0], CmdRunAction) + assert isinstance(runtime.run_action_calls[1], CmdRunAction) + + # Check that the first command is the git clone with the correct URL format without token + clone_cmd = runtime.run_action_calls[0].command + assert 'git clone https://github.com/owner/repo.git repo' in clone_cmd + + # Check that the second command is the checkout + checkout_cmd = runtime.run_action_calls[1].command + assert 'cd repo' in checkout_cmd + assert 'git checkout -b openhands-workspace-' in checkout_cmd - # Check that the command contains the correct URL format without token - cmd = runtime.run_action_calls[0].command - assert 'git clone https://github.com/owner/repo.git repo' in cmd - assert 'cd repo' in cmd - assert 'git checkout -b openhands-workspace-' in cmd assert result == 'repo' @@ -381,10 +400,23 @@ async def test_clone_or_init_repo_gitlab_with_token(temp_dir, monkeypatch): result = await runtime.clone_or_init_repo(git_provider_tokens, 'owner/repo', None) - cmd = runtime.run_action_calls[0].command + # Verify that git clone and checkout were called as separate commands + assert len(runtime.run_action_calls) == 2 + assert isinstance(runtime.run_action_calls[0], CmdRunAction) + assert isinstance(runtime.run_action_calls[1], CmdRunAction) + + # Check that the first command is the git clone with the correct URL format with token + clone_cmd = runtime.run_action_calls[0].command assert ( - f'git clone https://oauth2:{gitlab_token}@gitlab.com/owner/repo.git repo' in cmd + f'git clone https://oauth2:{gitlab_token}@gitlab.com/owner/repo.git repo' + in clone_cmd ) + + # Check that the second command is the checkout + checkout_cmd = runtime.run_action_calls[1].command + assert 'cd repo' in checkout_cmd + assert 'git checkout -b openhands-workspace-' in checkout_cmd + assert result == 'repo' @@ -402,14 +434,18 @@ async def test_clone_or_init_repo_with_branch(temp_dir, monkeypatch): mock_repo_and_patch(monkeypatch, provider=ProviderType.GITHUB) result = await runtime.clone_or_init_repo(None, 'owner/repo', 'feature-branch') - # Verify that git clone was called with the correct branch checkout - assert len(runtime.run_action_calls) == 1 + # Verify that git clone and checkout were called as separate commands + assert len(runtime.run_action_calls) == 2 assert isinstance(runtime.run_action_calls[0], CmdRunAction) + assert isinstance(runtime.run_action_calls[1], CmdRunAction) - # Check that the command contains the correct branch checkout - cmd = runtime.run_action_calls[0].command - assert 'git clone https://github.com/owner/repo.git repo' in cmd - assert 'cd repo' in cmd - assert 'git checkout feature-branch' in cmd - assert 'git checkout -b' not in cmd # Should not create a new branch + # Check that the first command is the git clone + clone_cmd = runtime.run_action_calls[0].command + + # Check that the second command contains the correct branch checkout + checkout_cmd = runtime.run_action_calls[1].command + assert 'git clone https://github.com/owner/repo.git repo' in clone_cmd + assert 'cd repo' in checkout_cmd + assert 'git checkout feature-branch' in checkout_cmd + assert 'git checkout -b' not in checkout_cmd # Should not create a new branch assert result == 'repo'