Compare commits

...

2 Commits

Author SHA1 Message Date
Graham Neubig
3c39b93f7e Fix CLI import error with broken third-party runtime dependencies (#9413)
Co-authored-by: openhands <openhands@all-hands.dev>
2025-06-27 12:03:48 -04:00
mamoodi
197e1a3612 Release 0.47.0 2025-06-26 15:09:28 -04:00
16 changed files with 195 additions and 28 deletions

View File

@@ -159,7 +159,7 @@ poetry run pytest ./tests/unit/test_*.py
To reduce build time (e.g., if no changes were made to the client-runtime component), you can use an existing Docker
container image by setting the SANDBOX_RUNTIME_CONTAINER_IMAGE environment variable to the desired Docker image.
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.46-nikolaik`
Example: `export SANDBOX_RUNTIME_CONTAINER_IMAGE=ghcr.io/all-hands-ai/runtime:0.47-nikolaik`
## Develop inside Docker container

View File

@@ -62,17 +62,17 @@ system requirements and more information.
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.46
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.

View File

@@ -51,17 +51,17 @@ OpenHands也可以使用Docker在本地系统上运行。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.46
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
> **注意**: 如果您在0.44版本之前使用过OpenHands您可能需要运行 `mv ~/.openhands-state ~/.openhands` 来将对话历史迁移到新位置。

View File

@@ -42,17 +42,17 @@ OpenHandsはDockerを利用してローカル環境でも実行できます。
> 公共ネットワークで実行していますか?[Hardened Docker Installation Guide](https://docs.all-hands.dev/usage/runtimes/docker#hardened-docker-installation)を参照して、ネットワークバインディングの制限や追加のセキュリティ対策を実施してください。
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.46
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
**注**: バージョン0.44以前のOpenHandsを使用していた場合は、会話履歴を移行するために `mv ~/.openhands-state ~/.openhands` を実行してください。

View File

@@ -12,7 +12,7 @@ services:
- SANDBOX_API_HOSTNAME=host.docker.internal
- DOCKER_HOST_ADDR=host.docker.internal
#
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.46-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-ghcr.io/all-hands-ai/runtime:0.47-nikolaik}
- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234}
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -7,7 +7,7 @@ services:
image: openhands:latest
container_name: openhands-app-${DATE:-}
environment:
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik}
- SANDBOX_RUNTIME_CONTAINER_IMAGE=${SANDBOX_RUNTIME_CONTAINER_IMAGE:-docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik}
#- SANDBOX_USER_ID=${SANDBOX_USER_ID:-1234} # enable this only if you want a specific non-root sandbox user but you will have to manually adjust permissions of ~/.openhands for this user
- WORKSPACE_MOUNT_PATH=${WORKSPACE_BASE:-$PWD/workspace}
ports:

View File

@@ -64,7 +64,7 @@ The conversation history will be saved in `~/.openhands/sessions`.
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -73,7 +73,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.46 \
docker.all-hands.dev/all-hands-ai/openhands:0.47 \
python -m openhands.cli.main --override-cli-mode true
```

View File

@@ -32,7 +32,7 @@ To run OpenHands in Headless mode with Docker:
```bash
docker run -it \
--pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e SANDBOX_USER_ID=$(id -u) \
-e SANDBOX_VOLUMES=$SANDBOX_VOLUMES \
-e LLM_API_KEY=$LLM_API_KEY \
@@ -42,7 +42,7 @@ docker run -it \
-v ~/.openhands:/.openhands \
--add-host host.docker.internal:host-gateway \
--name openhands-app-$(date +%Y%m%d%H%M%S) \
docker.all-hands.dev/all-hands-ai/openhands:0.46 \
docker.all-hands.dev/all-hands-ai/openhands:0.47 \
python -m openhands.core.main -t "write a bash script that prints hi"
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.

View File

@@ -68,23 +68,23 @@ Download and install the LM Studio desktop app from [lmstudio.ai](https://lmstud
1. Check [the installation guide](/usage/local-setup) and ensure all prerequisites are met before running OpenHands, then run:
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.46
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
2. Wait until the server is running (see log below):
```
Digest: sha256:e72f9baecb458aedb9afc2cd5bc935118d1868719e55d50da73190d3a85c674f
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.46
Status: Image is up to date for docker.all-hands.dev/all-hands-ai/openhands:0.47
Starting OpenHands...
Running OpenHands as root
14:22:13 - openhands:INFO: server_config.py:50 - Using config class None

View File

@@ -67,17 +67,17 @@ A system with a modern processor and a minimum of **4GB RAM** is recommended to
### Start the App
```bash
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik
docker pull docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik
docker run -it --rm --pull=always \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik \
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik \
-e LOG_ALL_EVENTS=true \
-v /var/run/docker.sock:/var/run/docker.sock \
-v ~/.openhands:/.openhands \
-p 3000:3000 \
--add-host host.docker.internal:host-gateway \
--name openhands-app \
docker.all-hands.dev/all-hands-ai/openhands:0.46
docker.all-hands.dev/all-hands-ai/openhands:0.47
```
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.

View File

@@ -1,12 +1,12 @@
{
"name": "openhands-frontend",
"version": "0.46.0",
"version": "0.47.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openhands-frontend",
"version": "0.46.0",
"version": "0.47.0",
"dependencies": {
"@heroui/react": "^2.8.0-beta.9",
"@microlink/react-json-view": "^1.26.2",

View File

@@ -1,6 +1,6 @@
{
"name": "openhands-frontend",
"version": "0.46.0",
"version": "0.47.0",
"private": true,
"type": "module",
"engines": {

View File

@@ -70,6 +70,13 @@ try:
_THIRD_PARTY_RUNTIME_CLASSES[runtime_name] = runtime_class
except ImportError:
# ImportError means the library is not installed (expected for optional dependencies)
pass
except Exception as e:
# Other exceptions mean the library is present but broken, which should be logged
from openhands.core.logger import openhands_logger as logger
logger.warning(f'Failed to import third-party runtime {module_path}: {e}')
pass
except ImportError:

View File

@@ -40,7 +40,7 @@ Two configuration options are required to use the Kubernetes runtime:
2. **Runtime Container Image**: Specify the container image to use for the runtime environment
```toml
[sandbox]
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.46-nikolaik"
runtime_container_image = "docker.all-hands.dev/all-hands-ai/runtime:0.47-nikolaik"
```
#### Additional Kubernetes Options

View File

@@ -6,7 +6,7 @@ requires = [
[tool.poetry]
name = "openhands-ai"
version = "0.46.0"
version = "0.47.0"
description = "OpenHands: Code Less, Make More"
authors = [ "OpenHands" ]
license = "MIT"

View File

@@ -0,0 +1,160 @@
"""
Test that the runtime import system is robust against broken third-party dependencies.
This test specifically addresses the issue where broken third-party runtime dependencies
(like runloop-api-client with incompatible httpx_aiohttp versions) would break the entire
OpenHands CLI and system.
"""
import logging
import sys
import pytest
def test_cli_import_with_broken_third_party_runtime():
"""Test that CLI can be imported even with broken third-party runtime dependencies."""
# Clear any cached modules to ensure fresh import
modules_to_clear = [
k for k in sys.modules.keys() if 'openhands' in k or 'third_party' in k
]
for module in modules_to_clear:
del sys.modules[module]
# This should not raise an exception even if third-party runtimes have broken dependencies
try:
import openhands.cli.main # noqa: F401
assert True
except Exception as e:
pytest.fail(f'CLI import failed: {e}')
def test_runtime_import_robustness():
"""Test that runtime import system is robust against broken dependencies."""
# Clear any cached runtime modules
modules_to_clear = [k for k in sys.modules.keys() if 'openhands.runtime' in k]
for module in modules_to_clear:
del sys.modules[module]
# Import the runtime module - should succeed even with broken third-party runtimes
try:
import openhands.runtime # noqa: F401
assert True
except Exception as e:
pytest.fail(f'Runtime import failed: {e}')
def test_get_runtime_cls_works():
"""Test that get_runtime_cls works even when third-party runtimes are broken."""
# Import the runtime module
import openhands.runtime
# Test that we can still get core runtime classes
docker_runtime = openhands.runtime.get_runtime_cls('docker')
assert docker_runtime is not None
local_runtime = openhands.runtime.get_runtime_cls('local')
assert local_runtime is not None
# Test that requesting a non-existent runtime raises appropriate error
with pytest.raises(ValueError, match='Runtime nonexistent not supported'):
openhands.runtime.get_runtime_cls('nonexistent')
def test_runtime_exception_handling():
"""Test that the runtime discovery code properly handles exceptions."""
# This test verifies that the fix in openhands/runtime/__init__.py
# properly catches all exceptions (not just ImportError) during
# third-party runtime discovery
import openhands.runtime
# The fact that we can import this module successfully means
# the exception handling is working correctly, even if there
# are broken third-party runtime dependencies
assert hasattr(openhands.runtime, 'get_runtime_cls')
assert hasattr(openhands.runtime, '_THIRD_PARTY_RUNTIME_CLASSES')
def test_runtime_import_exception_handling_behavior():
"""Test that runtime import handles ImportError silently but logs other exceptions."""
# Test the exception handling logic by simulating the exact code from runtime init
from io import StringIO
from openhands.core.logger import openhands_logger as logger
# Create a string buffer to capture log output
log_capture = StringIO()
handler = logging.StreamHandler(log_capture)
handler.setLevel(logging.WARNING)
# Add our test handler to the OpenHands logger
logger.addHandler(handler)
original_level = logger.level
logger.setLevel(logging.WARNING)
try:
# Test 1: ImportError should be handled silently (no logging)
module_path = 'third_party.runtime.impl.missing.missing_runtime'
try:
raise ImportError("No module named 'missing_library'")
except ImportError:
# This is the exact code from runtime init: just pass, no logging
pass
# Test 2: Other exceptions should be logged
module_path = 'third_party.runtime.impl.runloop.runloop_runtime'
try:
raise AttributeError(
"module 'httpx_aiohttp' has no attribute 'HttpxAiohttpClient'"
)
except ImportError:
# ImportError means the library is not installed (expected for optional dependencies)
pass
except Exception as e:
# Other exceptions mean the library is present but broken, which should be logged
# This is the exact code from runtime init
logger.warning(f'Failed to import third-party runtime {module_path}: {e}')
# Check the captured log output
log_output = log_capture.getvalue()
# Should contain the AttributeError warning
assert 'Failed to import third-party runtime' in log_output
assert 'HttpxAiohttpClient' in log_output
# Should NOT contain the ImportError message
assert 'missing_library' not in log_output
finally:
logger.removeHandler(handler)
logger.setLevel(original_level)
def test_import_error_handled_silently(caplog):
"""Test that ImportError is handled silently (no logging) as it means library is not installed."""
# Simulate the exact code path for ImportError
logging.getLogger('openhands.runtime')
with caplog.at_level(logging.WARNING):
# Simulate ImportError handling - this should NOT log anything
try:
raise ImportError("No module named 'optional_runtime_library'")
except ImportError:
# This is the exact code from runtime init: just pass, no logging
pass
# Check that NO warning was logged for ImportError
warning_records = [
record for record in caplog.records if record.levelname == 'WARNING'
]
assert len(warning_records) == 0, (
f'ImportError should not generate warnings, but got: {warning_records}'
)