mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
Add OpenHands app support on windows without WSL (#8674)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Boxuan Li <liboxuan@connect.hku.hk>
This commit is contained in:
@@ -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).
|
||||
|
||||
</details>
|
||||
|
||||
## Start the App
|
||||
|
||||
195
docs/modules/usage/windows-without-wsl.md
Normal file
195
docs/modules/usage/windows-without-wsl.md
Normal file
@@ -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.
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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}',
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
|
||||
45
poetry.lock
generated
45
poetry.lock
generated
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in New Issue
Block a user