feat: Add Windows PowerShell support to CLI runtime (#9211)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Graham Neubig 2025-06-22 20:17:40 -04:00 committed by GitHub
parent 081880248c
commit 5d69e606eb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 202 additions and 21 deletions

View File

@ -133,13 +133,66 @@ This guide provides step-by-step instructions for running OpenHands on a Windows
> **Note**: If you're running the frontend in development mode (using `npm run dev`), use port 3001 instead: `http://localhost:3001`
## Installing and Running the CLI
To install and run the OpenHands CLI on Windows without WSL, follow these steps:
### 1. Install uv (Python Package Manager)
Open PowerShell as Administrator and run:
```powershell
powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"
```
### 2. Install .NET SDK (Required)
The OpenHands CLI **requires** the .NET Core runtime for PowerShell integration. Without it, the CLI will fail to start with a `coreclr` error. Install the .NET SDK which includes the runtime:
```powershell
winget install Microsoft.DotNet.SDK.8
```
Alternatively, you can download and install the .NET SDK from the [official Microsoft website](https://dotnet.microsoft.com/download).
After installation, restart your PowerShell session to ensure the environment variables are updated.
### 3. Install and Run OpenHands
After installing the prerequisites, you can install and run OpenHands with:
```powershell
uvx --python 3.12 --from openhands-ai openhands
```
### Troubleshooting CLI Issues
#### CoreCLR Error
If you encounter an error like `Failed to load CoreCLR` or `pythonnet.load('coreclr')` when running OpenHands CLI, this indicates that the .NET Core runtime is missing or not properly configured. To fix this:
1. Install the .NET SDK as described in step 2 above
2. Verify that your system PATH includes the .NET SDK directories
3. Restart your PowerShell session completely after installing the .NET SDK
4. Make sure you're using PowerShell 7 (pwsh) rather than Windows PowerShell
To verify your .NET installation, run:
```powershell
dotnet --info
```
This should display information about your installed .NET SDKs and runtimes. If this command fails, the .NET SDK is not properly installed or not in your PATH.
If the issue persists after installing the .NET SDK, try installing the specific .NET Runtime version 6.0 or later from the [.NET download page](https://dotnet.microsoft.com/download).
## 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.
2. **.NET Core Requirement**: The PowerShell integration requires .NET Core Runtime to be installed. The CLI implementation attempts to load the CoreCLR at startup with `pythonnet.load('coreclr')` and will fail with an error if .NET Core is not properly installed.
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.

View File

@ -9,11 +9,12 @@ import select
import shutil
import signal
import subprocess
import sys
import tempfile
import time
import zipfile
from pathlib import Path
from typing import Any, Callable
from typing import TYPE_CHECKING, Any, Callable
from binaryornot.check import is_binary
from openhands_aci.editor.editor import OHEditor
@ -51,6 +52,41 @@ from openhands.runtime.base import Runtime
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.runtime_status import RuntimeStatus
if TYPE_CHECKING:
from openhands.runtime.utils.windows_bash import WindowsPowershellSession
# Import Windows PowerShell support if on Windows
if sys.platform == 'win32':
try:
from openhands.runtime.utils.windows_bash import WindowsPowershellSession
from openhands.runtime.utils.windows_exceptions import DotNetMissingError
except (ImportError, DotNetMissingError) as err:
# Print a user-friendly error message without stack trace
friendly_message = """
ERROR: PowerShell and .NET SDK are required but not properly configured
The .NET SDK and PowerShell are required for OpenHands CLI on Windows.
PowerShell integration cannot function without .NET Core.
Please install the .NET SDK by following the instructions at:
https://docs.all-hands.dev/usage/windows-without-wsl
After installing .NET SDK, restart your terminal and try again.
"""
print(friendly_message, file=sys.stderr)
logger.error(
f'Windows runtime initialization failed: {type(err).__name__}: {str(err)}'
)
if (
isinstance(err, DotNetMissingError)
and hasattr(err, 'details')
and err.details
):
logger.debug(f'Details: {err.details}')
# Exit the program with an error code
sys.exit(1)
class CLIRuntime(Runtime):
"""
@ -119,6 +155,10 @@ class CLIRuntime(Runtime):
self.file_editor = OHEditor(workspace_root=self._workspace_path)
self._shell_stream_callback: Callable[[str], None] | None = None
# Initialize PowerShell session on Windows
self._is_windows = sys.platform == 'win32'
self._powershell_session: WindowsPowershellSession | None = None
logger.warning(
'Initializing CLIRuntime. WARNING: NO SANDBOX IS USED. '
'This runtime executes commands directly on the local system. '
@ -135,6 +175,15 @@ class CLIRuntime(Runtime):
# Change to the workspace directory
os.chdir(self._workspace_path)
# Initialize PowerShell session if on Windows
if self._is_windows:
self._powershell_session = WindowsPowershellSession(
work_dir=self._workspace_path,
username=None, # Use current user
no_change_timeout_seconds=30,
max_memory_mb=None,
)
if not self.attach_to_existing:
await asyncio.to_thread(self.setup_initial_env)
@ -241,6 +290,40 @@ class CLIRuntime(Runtime):
except Exception as e:
logger.error(f'Error: {e}')
def _execute_powershell_command(
self, command: str, timeout: float
) -> CmdOutputObservation | ErrorObservation:
"""
Execute a command using PowerShell session on Windows.
Args:
command: The command to execute
timeout: Timeout in seconds for the command
Returns:
CmdOutputObservation containing the complete output and exit code
"""
if self._powershell_session is None:
return ErrorObservation(
content='PowerShell session is not available.',
error_id='POWERSHELL_SESSION_ERROR',
)
try:
# Create a CmdRunAction for the PowerShell session
from openhands.events.action import CmdRunAction
ps_action = CmdRunAction(command=command)
ps_action.set_hard_timeout(timeout)
# Execute the command using the PowerShell session
return self._powershell_session.execute(ps_action)
except Exception as e:
logger.error(f'Error executing PowerShell command "{command}": {e}')
return ErrorObservation(
content=f'Error executing PowerShell command "{command}": {str(e)}',
error_id='POWERSHELL_EXECUTION_ERROR',
)
def _execute_shell_command(
self, command: str, timeout: float
) -> CmdOutputObservation:
@ -378,9 +461,16 @@ class CLIRuntime(Runtime):
logger.debug(
f'Running command in CLIRuntime: "{action.command}" with effective timeout: {effective_timeout}s'
)
return self._execute_shell_command(
action.command, timeout=effective_timeout
)
# Use PowerShell on Windows if available, otherwise use subprocess
if self._is_windows and self._powershell_session is not None:
return self._execute_powershell_command(
action.command, timeout=effective_timeout
)
else:
return self._execute_shell_command(
action.command, timeout=effective_timeout
)
except Exception as e:
logger.error(
f'Error in CLIRuntime.run for command "{action.command}": {str(e)}'
@ -737,6 +827,16 @@ class CLIRuntime(Runtime):
raise RuntimeError(f'Error creating zip file: {str(e)}')
def close(self) -> None:
# Clean up PowerShell session if it exists
if self._powershell_session is not None:
try:
self._powershell_session.close()
logger.debug('PowerShell session closed successfully.')
except Exception as e:
logger.warning(f'Error closing PowerShell session: {e}')
finally:
self._powershell_session = None
self._runtime_initialized = False
super().close()

View File

@ -21,21 +21,31 @@ from openhands.events.observation.commands import (
CmdOutputObservation,
)
from openhands.runtime.utils.bash_constants import TIMEOUT_MESSAGE_TEMPLATE
from openhands.runtime.utils.windows_exceptions import DotNetMissingError
from openhands.utils.shutdown_listener import should_continue
pythonnet.load('coreclr')
logger.info("Successfully called pythonnet.load('coreclr')")
# Now that pythonnet is initialized, import clr and System
try:
import clr
pythonnet.load('coreclr')
logger.info("Successfully called pythonnet.load('coreclr')")
logger.debug(f'Imported clr module from: {clr.__file__}')
# Load System assembly *after* pythonnet is initialized
clr.AddReference('System')
import System
except Exception as clr_sys_ex:
raise RuntimeError(f'FATAL: Failed to import clr or System. Error: {clr_sys_ex}')
# Now that pythonnet is initialized, import clr and System
try:
import clr
logger.debug(f'Imported clr module from: {clr.__file__}')
# Load System assembly *after* pythonnet is initialized
clr.AddReference('System')
import System
except Exception as clr_sys_ex:
error_msg = 'Failed to import .NET components.'
details = str(clr_sys_ex)
logger.error(f'{error_msg} Details: {details}')
raise DotNetMissingError(error_msg, details)
except Exception as coreclr_ex:
error_msg = 'Failed to load CoreCLR.'
details = str(coreclr_ex)
logger.error(f'{error_msg} Details: {details}')
raise DotNetMissingError(error_msg, details)
# Attempt to load the PowerShell SDK assembly only if clr and System loaded
ps_sdk_path = None
@ -78,9 +88,10 @@ try:
RunspaceState,
)
except Exception as e:
raise RuntimeError(
f'FATAL: Failed to load PowerShell SDK components. Error: {e}. Check pythonnet installation and .NET Runtime compatibility. Path searched: {ps_sdk_path}'
)
error_msg = 'Failed to load PowerShell SDK components.'
details = f'{str(e)} (Path searched: {ps_sdk_path})'
logger.error(f'{error_msg} Details: {details}')
raise DotNetMissingError(error_msg, details)
class WindowsPowershellSession:
@ -115,9 +126,11 @@ class WindowsPowershellSession:
if PowerShell is None: # Check if SDK loading failed during module import
# Logged critical error during import, just raise here to prevent instantiation
raise RuntimeError(
'PowerShell SDK (System.Management.Automation.dll) could not be loaded. Cannot initialize WindowsPowershellSession.'
error_msg = (
'PowerShell SDK (System.Management.Automation.dll) could not be loaded.'
)
logger.error(error_msg)
raise DotNetMissingError(error_msg)
self.work_dir = os.path.abspath(work_dir)
self.username = username

View File

@ -0,0 +1,15 @@
"""
Custom exceptions for Windows-specific runtime issues.
"""
class DotNetMissingError(Exception):
"""
Exception raised when .NET SDK or CoreCLR is missing or cannot be loaded.
This is used to provide a cleaner error message to users without a full stack trace.
"""
def __init__(self, message: str, details: str | None = None):
self.message = message
self.details = details
super().__init__(message)