mirror of
https://github.com/OpenHands/OpenHands.git
synced 2026-10-07 16:19:05 +08:00
Removed V0 third party runtimes (#14119)
This commit is contained in:
@@ -71,9 +71,6 @@ These reference the V0 runtime image (`ghcr.io/openhands/runtime:X.Y-nikolaik`)
|
||||
### `enterprise/enterprise_local/README.md`
|
||||
- Uses `ghcr.io/openhands/runtime:main-nikolaik` (points to `main`, not versioned)
|
||||
|
||||
### `third_party/runtime/impl/daytona/README.md`
|
||||
- Uses `${OPENHANDS_VERSION}` variable, not hardcoded
|
||||
|
||||
## Image Registries
|
||||
|
||||
| Registry | Usage |
|
||||
|
||||
@@ -23,18 +23,6 @@ if [ -z "$WORKSPACE_MOUNT_PATH" ]; then
|
||||
unset WORKSPACE_BASE
|
||||
fi
|
||||
|
||||
if [[ "$INSTALL_THIRD_PARTY_RUNTIMES" == "true" ]]; then
|
||||
echo "Downloading and installing third_party_runtimes..."
|
||||
echo "Warning: Third-party runtimes are provided as-is, not actively supported and may be removed in future releases."
|
||||
|
||||
if pip install 'openhands-ai[third_party_runtimes]' -qqq 2> >(tee /dev/stderr); then
|
||||
echo "third_party_runtimes installed successfully."
|
||||
else
|
||||
echo "Failed to install third_party_runtimes." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ "$SANDBOX_USER_ID" -eq 0 ]]; then
|
||||
echo "Running OpenHands as root"
|
||||
export RUN_AS_OPENHANDS=false
|
||||
|
||||
@@ -10,7 +10,7 @@ strict_optional = True
|
||||
disable_error_code = type-abstract
|
||||
|
||||
# Exclude third-party runtime directory from type checking
|
||||
exclude = (third_party/|enterprise/)
|
||||
exclude = (enterprise/)
|
||||
|
||||
[mypy-openai.*]
|
||||
follow_imports = skip
|
||||
|
||||
Generated
+1
-4
@@ -6547,7 +6547,7 @@ python-docx = "*"
|
||||
python-dotenv = "*"
|
||||
python-frontmatter = ">=1.1"
|
||||
python-json-logger = ">=3.2.1"
|
||||
python-multipart = ">=0.0.22"
|
||||
python-multipart = ">=0.0.26"
|
||||
python-pptx = "*"
|
||||
python-socketio = "5.14"
|
||||
pythonnet = {version = "*", markers = "sys_platform == \"win32\""}
|
||||
@@ -6571,9 +6571,6 @@ uvicorn = "*"
|
||||
whatthepatch = ">=1.0.6"
|
||||
zope-interface = "7.2"
|
||||
|
||||
[package.extras]
|
||||
third-party-runtimes = ["daytona (==0.24.2)", "e2b-code-interpreter (>=2)", "modal (>=0.66.26,<1.2)", "runloop-api-client (==0.50)"]
|
||||
|
||||
[package.source]
|
||||
type = "directory"
|
||||
url = ".."
|
||||
|
||||
@@ -5,8 +5,6 @@
|
||||
# - V1 application server (in this repo): openhands/app_server/
|
||||
# Unless you are working on deprecation, please avoid extending this legacy file and consult the V1 codepaths above.
|
||||
# Tag: Legacy-V0
|
||||
import importlib
|
||||
|
||||
from openhands.runtime.base import Runtime
|
||||
from openhands.runtime.impl.cli.cli_runtime import CLIRuntime
|
||||
from openhands.runtime.impl.docker.docker_runtime import (
|
||||
@@ -18,7 +16,7 @@ from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime
|
||||
from openhands.utils.import_utils import get_impl
|
||||
|
||||
# mypy: disable-error-code="type-abstract"
|
||||
_DEFAULT_RUNTIME_CLASSES: dict[str, type[Runtime]] = {
|
||||
_ALL_RUNTIME_CLASSES: dict[str, type[Runtime]] = {
|
||||
'eventstream': DockerRuntime,
|
||||
'docker': DockerRuntime,
|
||||
'remote': RemoteRuntime,
|
||||
@@ -27,72 +25,6 @@ _DEFAULT_RUNTIME_CLASSES: dict[str, type[Runtime]] = {
|
||||
'cli': CLIRuntime,
|
||||
}
|
||||
|
||||
# Try to import third-party runtimes if available
|
||||
_THIRD_PARTY_RUNTIME_CLASSES: dict[str, type[Runtime]] = {}
|
||||
|
||||
# Dynamically discover and import third-party runtimes
|
||||
|
||||
# Check if third_party package exists and discover runtimes
|
||||
try:
|
||||
import third_party.runtime.impl
|
||||
|
||||
third_party_base = 'third_party.runtime.impl'
|
||||
|
||||
# List of potential third-party runtime modules to try
|
||||
# These are discovered from the third_party directory structure
|
||||
potential_runtimes = []
|
||||
try:
|
||||
import pkgutil
|
||||
|
||||
for importer, modname, ispkg in pkgutil.iter_modules(
|
||||
third_party.runtime.impl.__path__
|
||||
):
|
||||
if ispkg:
|
||||
potential_runtimes.append(modname)
|
||||
except Exception:
|
||||
# If discovery fails, no third-party runtimes will be loaded
|
||||
potential_runtimes = []
|
||||
|
||||
# Try to import each discovered runtime
|
||||
for runtime_name in potential_runtimes:
|
||||
try:
|
||||
module_path = f'{third_party_base}.{runtime_name}.{runtime_name}_runtime'
|
||||
module = importlib.import_module(module_path)
|
||||
|
||||
# Try different class name patterns
|
||||
possible_class_names = [
|
||||
f'{runtime_name.upper()}Runtime', # E2BRuntime
|
||||
f'{runtime_name.capitalize()}Runtime', # E2bRuntime, DaytonaRuntime, etc.
|
||||
]
|
||||
|
||||
runtime_class = None
|
||||
for class_name in possible_class_names:
|
||||
try:
|
||||
runtime_class = getattr(module, class_name)
|
||||
break
|
||||
except AttributeError:
|
||||
continue
|
||||
|
||||
if runtime_class:
|
||||
_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:
|
||||
# third_party package not available
|
||||
pass
|
||||
|
||||
# Combine core and third-party runtimes
|
||||
_ALL_RUNTIME_CLASSES = {**_DEFAULT_RUNTIME_CLASSES, **_THIRD_PARTY_RUNTIME_CLASSES}
|
||||
|
||||
|
||||
def get_runtime_cls(name: str) -> type[Runtime]:
|
||||
"""If name is one of the predefined runtime names (e.g. 'docker'), return its class.
|
||||
@@ -120,7 +52,3 @@ __all__ = [
|
||||
'LocalRuntime',
|
||||
'get_runtime_cls',
|
||||
]
|
||||
|
||||
# Add third-party runtimes to __all__ if they're available
|
||||
for runtime_name, runtime_class in _THIRD_PARTY_RUNTIME_CLASSES.items():
|
||||
__all__.append(runtime_class.__name__)
|
||||
|
||||
Generated
+10
-461
@@ -1,4 +1,4 @@
|
||||
# This file is automatically @generated by Poetry 2.3.3 and should not be changed by hand.
|
||||
# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand.
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
@@ -199,22 +199,6 @@ yarl = ">=1.17.0,<2.0"
|
||||
[package.extras]
|
||||
speedups = ["Brotli (>=1.2) ; platform_python_implementation == \"CPython\"", "aiodns (>=3.3.0)", "backports.zstd ; platform_python_implementation == \"CPython\" and python_version < \"3.14\"", "brotlicffi (>=1.2) ; platform_python_implementation != \"CPython\""]
|
||||
|
||||
[[package]]
|
||||
name = "aiohttp-retry"
|
||||
version = "2.9.1"
|
||||
description = "Simple retry client for aiohttp"
|
||||
optional = true
|
||||
python-versions = ">=3.7"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "aiohttp_retry-2.9.1-py3-none-any.whl", hash = "sha256:66d2759d1921838256a05a3f80ad7e724936f083e35be5abb5e16eed6be6dc54"},
|
||||
{file = "aiohttp_retry-2.9.1.tar.gz", hash = "sha256:8eb75e904ed4ee5c2ec242fefe85bf04240f685391c4879d8f541d6028ff01f1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = "*"
|
||||
|
||||
[[package]]
|
||||
name = "aiosignal"
|
||||
version = "1.4.0"
|
||||
@@ -789,19 +773,6 @@ urllib3 = {version = ">=1.25.4,<2.2.0 || >2.2.0,<3", markers = "python_version >
|
||||
[package.extras]
|
||||
crt = ["awscrt (==0.31.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "bracex"
|
||||
version = "2.6"
|
||||
description = "Bash style brace expander."
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "bracex-2.6-py3-none-any.whl", hash = "sha256:0b0049264e7340b3ec782b5cb99beb325f36c3782a32e36e876452fd49a09952"},
|
||||
{file = "bracex-2.6.tar.gz", hash = "sha256:98f1347cd77e22ee8d967a30ad4e310b233f7754dbf31ff3fceb76145ba47dc7"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "browser-use"
|
||||
version = "0.11.13"
|
||||
@@ -1822,70 +1793,6 @@ files = [
|
||||
{file = "cython-3.2.4.tar.gz", hash = "sha256:84226ecd313b233da27dc2eb3601b4f222b8209c3a7216d8733b031da1dc64e6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "daytona"
|
||||
version = "0.24.2"
|
||||
description = "Python SDK for Daytona"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "daytona-0.24.2-py3-none-any.whl", hash = "sha256:6920aa6b52c0cf8c44722938469801727d9bf58a10a504833a703abce7559fef"},
|
||||
{file = "daytona-0.24.2.tar.gz", hash = "sha256:c9217b7198f1855c5298191dfa9b8d5f663e8e94e6fcf79a515424696ce1d1e1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiofiles = ">=24.1.0,<24.2.0"
|
||||
daytona-api-client = "0.24.2"
|
||||
daytona-api-client-async = "0.24.2"
|
||||
Deprecated = ">=1.2.18,<2.0.0"
|
||||
environs = ">=10.0.0,<15.0.0"
|
||||
httpx = ">=0.28.0,<0.29.0"
|
||||
obstore = ">=0.7.0,<0.8.0"
|
||||
pydantic = ">=2.4.2,<3.0.0"
|
||||
toml = ">=0.10.0,<0.11.0"
|
||||
|
||||
[[package]]
|
||||
name = "daytona-api-client"
|
||||
version = "0.24.2"
|
||||
description = "Daytona"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "daytona_api_client-0.24.2-py3-none-any.whl", hash = "sha256:eadb133f51611dc701709b03bc79257fd027708a62493ce2cf90279b25e63e91"},
|
||||
{file = "daytona_api_client-0.24.2.tar.gz", hash = "sha256:20b4631496b18b3a366e50ad14cbcdf1b619c23f64cfd0caeecdab64e4f1dac5"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
pydantic = ">=2"
|
||||
python-dateutil = ">=2.8.2"
|
||||
typing-extensions = ">=4.7.1"
|
||||
urllib3 = ">=1.25.3,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "daytona-api-client-async"
|
||||
version = "0.24.2"
|
||||
description = "Daytona"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "daytona_api_client_async-0.24.2-py3-none-any.whl", hash = "sha256:e2300711d5bcf5b71350810b213152f9c364c7fe7d478a60ab093a4c8e149cb5"},
|
||||
{file = "daytona_api_client_async-0.24.2.tar.gz", hash = "sha256:4f54c993d402c617ab2cf347e84a1414474408fe7d0f1cdc255c09026dbd610f"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = ">=3.8.4"
|
||||
aiohttp-retry = ">=2.8.3"
|
||||
pydantic = ">=2"
|
||||
python-dateutil = ">=2.8.2"
|
||||
typing-extensions = ">=4.7.1"
|
||||
urllib3 = ">=1.25.3,<3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "debugpy"
|
||||
version = "1.8.20"
|
||||
@@ -2066,19 +1973,6 @@ docs = ["myst-parser (==0.18.0)", "sphinx (==5.1.1)"]
|
||||
ssh = ["paramiko (>=2.4.3)"]
|
||||
websockets = ["websocket-client (>=1.3.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "dockerfile-parse"
|
||||
version = "2.0.1"
|
||||
description = "Python library for Dockerfile manipulation"
|
||||
optional = true
|
||||
python-versions = ">=3.6"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "dockerfile-parse-2.0.1.tar.gz", hash = "sha256:3184ccdc513221983e503ac00e1aa504a2aa8f84e5de673c46b0b6eee99ec7bc"},
|
||||
{file = "dockerfile_parse-2.0.1-py2.py3-none-any.whl", hash = "sha256:bdffd126d2eb26acf1066acb54cb2e336682e1d72b974a40894fac76a4df17f6"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "docstring-parser"
|
||||
version = "0.17.0"
|
||||
@@ -2181,49 +2075,6 @@ files = [
|
||||
{file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "e2b"
|
||||
version = "2.20.0"
|
||||
description = "E2B SDK that give agents cloud environments"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.10"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "e2b-2.20.0-py3-none-any.whl", hash = "sha256:66f6edcf6b742ca180f3aadcff7966fda86d68430fa6b2becdfa0fcc72224988"},
|
||||
{file = "e2b-2.20.0.tar.gz", hash = "sha256:52b3a00ac7015bbdce84913b2a57664d2def33d5a4069e34fa2354de31759173"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
attrs = ">=23.2.0"
|
||||
dockerfile-parse = ">=2.0.1,<3.0.0"
|
||||
httpcore = ">=1.0.5,<2.0.0"
|
||||
httpx = ">=0.27.0,<1.0.0"
|
||||
packaging = ">=24.1"
|
||||
protobuf = ">=4.21.0"
|
||||
python-dateutil = ">=2.8.2"
|
||||
rich = ">=14.0.0"
|
||||
typing-extensions = ">=4.1.0"
|
||||
wcmatch = ">=10.1,<11.0"
|
||||
|
||||
[[package]]
|
||||
name = "e2b-code-interpreter"
|
||||
version = "2.6.0"
|
||||
description = "E2B Code Interpreter - Stateful code execution"
|
||||
optional = true
|
||||
python-versions = "<4.0,>=3.10"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "e2b_code_interpreter-2.6.0-py3-none-any.whl", hash = "sha256:a15f1d155566aef98cf2ccc0f8d9b07d15e07582d6cc8a128bc97de371bd617c"},
|
||||
{file = "e2b_code_interpreter-2.6.0.tar.gz", hash = "sha256:67e66531e5cf65c9df6e82aa0bdb1e73223a1ab205f10d47c027eb2ea09b73f9"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
attrs = ">=21.3.0"
|
||||
e2b = ">=2.7.0,<3.0.0"
|
||||
httpx = ">=0.20.0,<1.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "email-validator"
|
||||
version = "2.3.0"
|
||||
@@ -2240,28 +2091,6 @@ files = [
|
||||
dnspython = ">=2.0.0"
|
||||
idna = ">=2.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "environs"
|
||||
version = "14.6.0"
|
||||
description = "simplified environment variable parsing"
|
||||
optional = true
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "environs-14.6.0-py3-none-any.whl", hash = "sha256:f8fb3d6c6a55872b0c6db077a28f5a8c7b8984b7c32029613d44cef95cfc0812"},
|
||||
{file = "environs-14.6.0.tar.gz", hash = "sha256:ed2767588deb503209ffe4dd9bb2b39311c2e4e7e27ce2c64bf62ca83328d068"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
marshmallow = ">=3.26.2"
|
||||
python-dotenv = "*"
|
||||
|
||||
[package.extras]
|
||||
dev = ["environs[tests]", "pre-commit (>=4.0,<5.0)", "tox"]
|
||||
django = ["dj-database-url", "dj-email-url", "django-cache-url"]
|
||||
tests = ["backports.strenum ; python_version < \"3.11\"", "environs[django]", "packaging", "pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "et-xmlfile"
|
||||
version = "2.0.0"
|
||||
@@ -3537,27 +3366,7 @@ files = [
|
||||
[package.dependencies]
|
||||
googleapis-common-protos = ">=1.5.5"
|
||||
grpcio = ">=1.67.1"
|
||||
protobuf = ">=5.26.1,<6.0.dev0"
|
||||
|
||||
[[package]]
|
||||
name = "grpclib"
|
||||
version = "0.4.8"
|
||||
description = "Pure-Python gRPC implementation for asyncio"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "grpclib-0.4.8-py3-none-any.whl", hash = "sha256:a5047733a7acc1c1cee6abf3c841c7c6fab67d2844a45a853b113fa2e6cd2654"},
|
||||
{file = "grpclib-0.4.8.tar.gz", hash = "sha256:d8823763780ef94fed8b2c562f7485cf0bbee15fc7d065a640673667f7719c9a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
h2 = ">=3.1.0,<5"
|
||||
multidict = "*"
|
||||
|
||||
[package.extras]
|
||||
protobuf = ["protobuf (>=3.20.0)"]
|
||||
protobuf = ">=5.26.1,<6.0dev"
|
||||
|
||||
[[package]]
|
||||
name = "gymnasium"
|
||||
@@ -3602,23 +3411,6 @@ files = [
|
||||
{file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "h2"
|
||||
version = "4.3.0"
|
||||
description = "Pure-Python HTTP/2 protocol implementation"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd"},
|
||||
{file = "h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
hpack = ">=4.1,<5"
|
||||
hyperframe = ">=6.1,<7"
|
||||
|
||||
[[package]]
|
||||
name = "hf-xet"
|
||||
version = "1.4.3"
|
||||
@@ -3658,19 +3450,6 @@ files = [
|
||||
[package.extras]
|
||||
tests = ["pytest"]
|
||||
|
||||
[[package]]
|
||||
name = "hpack"
|
||||
version = "4.1.0"
|
||||
description = "Pure-Python HPACK header encoding"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"},
|
||||
{file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "html2text"
|
||||
version = "2025.4.15"
|
||||
@@ -3809,19 +3588,6 @@ testing = ["Jinja2", "Pillow", "authlib (>=1.3.2)", "duckdb", "fastapi", "fastap
|
||||
torch = ["safetensors[torch]", "torch"]
|
||||
typing = ["types-PyYAML", "types-simplejson", "types-toml", "types-tqdm", "types-urllib3", "typing-extensions (>=4.8.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "hyperframe"
|
||||
version = "6.1.0"
|
||||
description = "Pure-Python HTTP/2 framing"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"},
|
||||
{file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "identify"
|
||||
version = "2.6.18"
|
||||
@@ -3905,7 +3671,7 @@ pfzy = ">=0.3.1,<0.4.0"
|
||||
prompt-toolkit = ">=3.0.1,<4.0.0"
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17b43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"]
|
||||
docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "installer"
|
||||
@@ -4359,7 +4125,7 @@ fqdn = {version = "*", optional = true, markers = "extra == \"format-nongpl\""}
|
||||
idna = {version = "*", optional = true, markers = "extra == \"format-nongpl\""}
|
||||
isoduration = {version = "*", optional = true, markers = "extra == \"format-nongpl\""}
|
||||
jsonpointer = {version = ">1.13", optional = true, markers = "extra == \"format-nongpl\""}
|
||||
jsonschema-specifications = ">=2023.3.6"
|
||||
jsonschema-specifications = ">=2023.03.6"
|
||||
referencing = ">=0.28.4"
|
||||
rfc3339-validator = {version = "*", optional = true, markers = "extra == \"format-nongpl\""}
|
||||
rfc3986-validator = {version = ">0.1.0", optional = true, markers = "extra == \"format-nongpl\""}
|
||||
@@ -4842,7 +4608,7 @@ files = [
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
certifi = ">=14.5.14"
|
||||
certifi = ">=14.05.14"
|
||||
durationpy = ">=0.7"
|
||||
google-auth = ">=1.0.1"
|
||||
oauthlib = ">=3.2.2"
|
||||
@@ -5585,19 +5351,6 @@ files = [
|
||||
{file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "marshmallow"
|
||||
version = "4.2.4"
|
||||
description = "A lightweight library for converting complex datatypes to and from native Python datatypes."
|
||||
optional = true
|
||||
python-versions = ">=3.10"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "marshmallow-4.2.4-py3-none-any.whl", hash = "sha256:06f3a7ec7081e54e8026a8bd5e7c4e0e88bff82a8fa33714a33166917f2b353a"},
|
||||
{file = "marshmallow-4.2.4.tar.gz", hash = "sha256:1ab9c798651b9048d127969d9423a0e47510e34100df1d3e24c9d5b6cdc26a86"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "matplotlib"
|
||||
version = "3.10.8"
|
||||
@@ -5779,34 +5532,6 @@ files = [
|
||||
{file = "mistune-3.2.0.tar.gz", hash = "sha256:708487c8a8cdd99c9d90eb3ed4c3ed961246ff78ac82f03418f5183ab70e398a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "modal"
|
||||
version = "1.1.4"
|
||||
description = "Python client library for Modal"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "modal-1.1.4-py3-none-any.whl", hash = "sha256:fcacbc22c5db59b76239f54e16e35c5a4a24cda10b465f1e4750311418939212"},
|
||||
{file = "modal-1.1.4.tar.gz", hash = "sha256:67f612f41ef6091e24a92fb9bb158fabf5e8f6ff7351a2c7ae2140bb112b871b"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
aiohttp = "*"
|
||||
certifi = "*"
|
||||
click = ">=8.1,<9.0"
|
||||
grpclib = ">=0.4.7,<0.4.9"
|
||||
protobuf = ">=3.19,<4.24.0 || >4.24.0,<7.0"
|
||||
rich = ">=12.0.0"
|
||||
synchronicity = ">=0.10.2,<0.11.0"
|
||||
toml = "*"
|
||||
typer = ">=0.9"
|
||||
types-certifi = "*"
|
||||
types-toml = "*"
|
||||
typing_extensions = ">=4.6,<5.0"
|
||||
watchfiles = "*"
|
||||
|
||||
[[package]]
|
||||
name = "more-itertools"
|
||||
version = "11.0.1"
|
||||
@@ -6383,97 +6108,6 @@ rsa = ["cryptography (>=3.0.0)"]
|
||||
signals = ["blinker (>=1.4.0)"]
|
||||
signedtoken = ["cryptography (>=3.0.0)", "pyjwt (>=2.0.0,<3)"]
|
||||
|
||||
[[package]]
|
||||
name = "obstore"
|
||||
version = "0.7.3"
|
||||
description = ""
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "obstore-0.7.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:8c89b6205672490fb99e16159bb290a12d4d8e6f9b27904720faafd4fd8ae436"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:26357df7b3824f431ced44e26fe334f686410cb5e8c218569759d6aa32ab7242"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ca3380121cc5ce6d040698fcf126c1acab4a00282db5a6bc8e5026bba22fc43d"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c1eca930fa0229f7fd5d881bc03deffca51e96ad754cbf256e4aa27ac7c50db6"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e4b91fec58a65350303b643ce1da7a890fb2cc411c2a9d86672ad30febb196df"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4eba1c87af7002d95cce8c2c67fac814056938f16500880e1fb908a0e8c7a7f5"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd5e8ad65c5b481f168080db1c5290cf55ad7ab77b45fd467c4d25367db2a3ae"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-manylinux_2_24_aarch64.whl", hash = "sha256:b680dd856d238a892a14ef3115daee33e267502229cee248266a20e03dbe98d0"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c3dccb74ebfec1f5517c2160503f30629b62685c78bbe15ad03492969fadd858"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:cd614e53a00d22b2facfd1fb9b516fa210cd788ecce513dd532a8e65fa07d55d"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:32841a2b4bef838412302e9a8612fc3ba1c51bd808b77b4854efe6b1f7a65f0d"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a58f3952b43fb5f7b0f0f9f08272983e4dd50f83b16a05943f89581b0e6bff20"},
|
||||
{file = "obstore-0.7.3-cp310-cp310-win_amd64.whl", hash = "sha256:8745e2437e79e073c3cf839454f803909540fa4f6cd9180c9ab4ce742c716c8b"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:65ffe43fd63c9968172bed649fcaf6345b41a124be5d34f46adb94604e9ccef8"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2947609a1fab1f9b808235a8088e7e99814fbaf3b6000833d760fd90f68fa7cd"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:15409f75acc4e10f924fe118f7018607d6d96a72330ac4cc1663d36b7c6847b1"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5224d834bbe7a9f2592b130e4ddd86340fa172e5a3a51284e706f6515d95c036"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7b1af6c1a33d98db9954f7ceab8eb5e543aea683a79a0ffd72b6c8d176834a9b"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:708c27c4e5e85799fe7a2d2ae443fbd96c2ad36b561c815a9b01b5333ab536ad"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7da327920bef8bbd02445f33947487fe4e94fcb9e084c810108e88be57d0877b"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-manylinux_2_24_aarch64.whl", hash = "sha256:8f3b23a40ad374fe7a65fab4678a9978978ec83a597156a2a9d1dbeab433a469"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9b3e7d0c7e85e4f67e479f7efab5dea26ceaace10897d639d38f77831ef0cdaf"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:dfee24c5e9d5b7e0f43e4bbf8cc15069e5c60bfdb86873ce97c0eb487afa5da8"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:99e187cee4a6e13605886b906b34fec7ae9902dd25b1e9aafae863a9d55c6e47"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5de3b0859512b9ddbf57ac34db96ad41fb85fc9597e422916044d1bf550427d"},
|
||||
{file = "obstore-0.7.3-cp311-cp311-win_amd64.whl", hash = "sha256:35fdd1cd8856984de1b5a11fced83f6fd6623eb459736e57b9975400ff5baf5a"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:6cbe5dde68bf6ab5a88f3bb467ca8f123bcce3efc03e22fd8339688559d36199"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:a6db23cbcb3aec10e09a31fd0883950cb9b7f77f4fcf1fb0e8a276e1d1961bf3"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:00fde287770bdbdbb06379670d30c257b20e77a4a11b36f1e232b5bc6ef07b7a"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c420036356269666197f0704392c9495f255bb3ff9b667c69fb49bc65bd50dcd"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c28482626ca9481569ad16ba0c0c36947ce96e8147c64011dc0af6d58be8ff9c"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9cead20055221337ddf218098afe8138f8624395b0cf2a730da72a4523c11b2f"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c71017142a593022848f4af0ac1e39af1a56927981cc2c89542888edb206eb33"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-manylinux_2_24_aarch64.whl", hash = "sha256:8aebc2bf796a0d1525318a9ac69608a96d03abc621ca1e6d810e08a70bd695c1"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c0ebf03969b81ee559c377c5ebca9dcdffbef0e6650d43659676aeaeb302a272"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e0f5d97064ec35fdef3079f867afe6fa5e76ab2bb3e809855ab34a1aa34c9dcd"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3a80541671646c5e49493de61361a1851c8c172cf28981b76aa4248a9f02f5b1"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a5ce6385ad89afad106d05d37296f724ba10f8f4e57ab8ad7f4ecce0aa226d3d"},
|
||||
{file = "obstore-0.7.3-cp312-cp312-win_amd64.whl", hash = "sha256:632522ba63a44768977defc0a93fc5dd59ea0455bfd6926cd3121971306da4e5"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:dcb71412dc8d2bd464b340d1f36d8c0ceb7894c01c2ceaaa5f2ac45376503fa2"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6d486bb01438039d686401ce4207d82c02b8b639227baa5bdd578efdab388dea"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaaf0c9223b5592658c131ff32a0574be995c7e237f406266f9a68ea2266769"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8ae6cde734df3cc542c14152029170d9ae70ce50b957831ed71073113bd3d60"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:30da82ae3bfdf24fa80af38967e323ae8da0bb7c36cce01f0dda7689faaf1272"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b5daa9f912eac8cdf218161d34e13f38cbb594e934eaaf8a7c09dca5a394b231"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1ef06cad4e8978d672357b328b4f61c48827b2b79d7eaf58b68ee31ac0e652b8"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-manylinux_2_24_aarch64.whl", hash = "sha256:d34920539a94da2b87195787b80004960638dfd0aa2f4369fc9239e0a41470a8"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6dfcdaa779f376745ff493cce7f19cbbe8d75f68304bf1062e757ab60bd62de1"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ae095f679e4796b8f6ef80ed3813ddd14a477ae219a0c059c23cf294f9288ded"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:6def59e79c19b8804743fec6407f542b387dc1630c2254412ae8bd3a0b98e7e4"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f97797c42476ab19853ef4a161b903eaf96c2363a23b9e0187d66b0daee350cb"},
|
||||
{file = "obstore-0.7.3-cp313-cp313-win_amd64.whl", hash = "sha256:8f0ecc01b1444bc08ff98e368b80ea2c085a7783621075298e86d3aba96f8e27"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:b0a337b6d2b430040e752effdf9584b0d6adddef2ead2bbbc3c204957a2f69d2"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:439874c31a78198211c45ebde0b3535650dc3585353be51b361bd017bc492090"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:360034e4b1fe84da59bc3b090798acdd1b4a8b75cc1e56d2656591c7cc8776f2"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44989c9be1156c8ad02522bcb0358e813fd71fa061e51c3331cc11f4b6d36525"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3bf0b9c28b3149138ff3db0c2cfb3acb329d3a3bef02a3146edec6d2419b27ad"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98fd91e90442ff3bf8832c713189c81cd892299a8423fc5d8c4534e84db62643"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9eccae18d75d753129d58c080716cd91738fd1f913b7182eb5695f483d6cbd94"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-manylinux_2_24_aarch64.whl", hash = "sha256:bbe0488ca1573020af14ca585ddc5e5aa7593f8fc42ec5d1f53b83393ccaefa5"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:6765cef76ca62b13d4cfec4648fbf6048410d34c2e11455323d011d208977b89"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:00f8d1211d247fc24c9f5d5614f2ed25872fe2c4af2e283f3e6cc85544a3dee5"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ebc387320a00918c8afb5f2d76c07157003a661d60ff03763103278670bc75e3"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:8b526bdc5b5392ac55b3a45bf04f2eba3a33c132dfa04418e7ffba38763d7b5d"},
|
||||
{file = "obstore-0.7.3-cp39-cp39-win_amd64.whl", hash = "sha256:1af6dfef86b37e74ff812bd70d8643619e16485559fcaee01b3f2442b70d4918"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:848eb12ed713f447a7b1f7de3f0bff570de99546f76c37e6315102f5bbdaf71c"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:091998d57331aa0e648a9dca0adebf6dc09eb53a4e6935c9c06625998120acc1"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed7c957d19a6a994e8c9198b1e58b31e0fc3748ca056e27f738a4ead789eb80b"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:af8daa0568c89ce863986ccf14570c30d1dc817b51ed2146eecb76fddc82704e"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fe42053413a35a964e88ea156af3253defac30bedd973797b55b8e230cc50fe4"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d2faa2ac90672334cdaabbf930c82e91efa184928dc55b55bcbf84b152bc4df1"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49f20fdabd295a5a001569957c19a51615d288cd255fb80dcf966e2307ca0cec"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-manylinux_2_24_aarch64.whl", hash = "sha256:aa131d089565fb7a5225220fcdfe260e3b1fc6821c0a2eef2e3a23c5ba9c79bd"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:73df8b270b89a97ef9e87fc8e552d97d426bbfcb61c55097f5d452a7457ee9d5"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:25cea5cf5a727800b14cf4d09fd2b799c28fb755cc04e5635e7fb36d413bf772"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:aae7fea048d7e73e5c206efef1627bff677455f6eed5c94a596906c4fcedc744"},
|
||||
{file = "obstore-0.7.3-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:b4ee1ee4f8846ae891f1715a19a8f89d16a00c9e8913bf60c9f3acf24d905de2"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = {version = "*", markers = "python_full_version < \"3.13.0\""}
|
||||
|
||||
[[package]]
|
||||
name = "ollama"
|
||||
version = "0.6.1"
|
||||
@@ -7135,7 +6769,6 @@ files = [
|
||||
{file = "pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523"},
|
||||
{file = "pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f"},
|
||||
]
|
||||
markers = {runtime = "sys_platform != \"win32\" and sys_platform != \"emscripten\""}
|
||||
|
||||
[package.dependencies]
|
||||
ptyprocess = ">=0.5"
|
||||
@@ -7153,7 +6786,7 @@ files = [
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17b43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"]
|
||||
docs = ["Sphinx (>=4.1.2,<5.0.0)", "furo (>=2021.8.17-beta.43,<2022.0.0)", "myst-parser (>=0.15.1,<0.16.0)", "sphinx-autobuild (>=2021.3.14,<2022.0.0)", "sphinx-copybutton (>=0.4.0,<0.5.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "pg8000"
|
||||
@@ -7705,7 +7338,6 @@ files = [
|
||||
{file = "ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35"},
|
||||
{file = "ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220"},
|
||||
]
|
||||
markers = {runtime = "sys_platform != \"win32\" and sys_platform != \"emscripten\" or os_name != \"nt\""}
|
||||
|
||||
[[package]]
|
||||
name = "pure-eval"
|
||||
@@ -13038,30 +12670,6 @@ files = [
|
||||
{file = "ruff-0.12.5.tar.gz", hash = "sha256:b209db6102b66f13625940b7f8c7d0f18e20039bb7f6101fbdac935c9612057e"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "runloop-api-client"
|
||||
version = "0.50.0"
|
||||
description = "The official Python library for the runloop API"
|
||||
optional = true
|
||||
python-versions = ">=3.8"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "runloop_api_client-0.50.0-py3-none-any.whl", hash = "sha256:4fec23676f1b560c2740604c56fd0f8e52d7b2892e42157ba1c291a26c8ebced"},
|
||||
{file = "runloop_api_client-0.50.0.tar.gz", hash = "sha256:a967f3daf6248d9256be544bb5f557557d716d898d798794c0142408a374299a"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
anyio = ">=3.5.0,<5"
|
||||
distro = ">=1.7.0,<2"
|
||||
httpx = ">=0.23.0,<1"
|
||||
pydantic = ">=1.9.0,<3"
|
||||
sniffio = "*"
|
||||
typing-extensions = ">=4.10,<5"
|
||||
|
||||
[package.extras]
|
||||
aiohttp = ["aiohttp", "httpx-aiohttp (>=0.1.8)"]
|
||||
|
||||
[[package]]
|
||||
name = "s3transfer"
|
||||
version = "0.16.0"
|
||||
@@ -13075,10 +12683,10 @@ files = [
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
botocore = ">=1.37.4,<2.0a0"
|
||||
botocore = ">=1.37.4,<2.0a.0"
|
||||
|
||||
[package.extras]
|
||||
crt = ["botocore[crt] (>=1.37.4,<2.0a0)"]
|
||||
crt = ["botocore[crt] (>=1.37.4,<2.0a.0)"]
|
||||
|
||||
[[package]]
|
||||
name = "scantree"
|
||||
@@ -13507,25 +13115,6 @@ typing-extensions = {version = ">=4.10.0", markers = "python_version < \"3.13\""
|
||||
[package.extras]
|
||||
full = ["httpx (>=0.27.0,<0.29.0)", "itsdangerous", "jinja2", "python-multipart (>=0.0.18)", "pyyaml"]
|
||||
|
||||
[[package]]
|
||||
name = "synchronicity"
|
||||
version = "0.10.5"
|
||||
description = "Export blocking and async library versions from a single async implementation"
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "synchronicity-0.10.5-py3-none-any.whl", hash = "sha256:8bcdbfe6f9456ddcfcb267e0ca853c3b7650f129acf3abae4af45caf85d66d16"},
|
||||
{file = "synchronicity-0.10.5.tar.gz", hash = "sha256:6bdb3ea99f327e2d5602ad134458244ddaf331d56951634e5424df1edb07fe0d"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
typing-extensions = ">=4.12.2"
|
||||
|
||||
[package.extras]
|
||||
compile = ["sigtools (>=4.0.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "tenacity"
|
||||
version = "9.1.4"
|
||||
@@ -13884,14 +13473,6 @@ optional = false
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
files = [
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_10_9_x86_64.whl", hash = "sha256:e87be7572991552606a3155d2f6c2045ded8bce94bfd9f74bf521d949c219a1c"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp310-abi3-macosx_11_0_arm64.whl", hash = "sha256:86c2fdf178c66474a1be2965602818d30780e4e3ed890e3c206931f65d9a154c"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:035d259e64c41d02cc45afc3b8b46388b232e7d16d84734d851cca7334761da5"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp310-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fa472cb9de7e14fee9408e144f29f68384cd8e9c677dff0002da19f361a59bdf"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1a0ea86eccff74e85ab4a2cf77c813fad7c84162962ce242dff0c51601028832"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp310-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:8ab26dc998bbd4b4287b129f67c10ca715deb402ed77d0645674490ea509097e"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp310-abi3-win_amd64.whl", hash = "sha256:d4486653feaff3314ef45534dcb6f9ea8ab3aa160896287c6473788f88eb38be"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp310-abi3-win_arm64.whl", hash = "sha256:e7a14b76ec23cc8386cf662d5ea602d81331376c93ca6299a97b174047790345"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:2b612a6e5bd17bb7fa2aab4bb6fc1fba45c94f09cb034ab332e45603b86e32fd"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a8b98f62bc53efcd4d971151950c9b9cd5cbe3bacdb0cd69fdccac63350d83e"},
|
||||
{file = "tree_sitter_c_sharp-0.23.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:986e93d845a438ec3c4416401aa98e6a6f6631d644bbbc2e43fcb915c51d255d"},
|
||||
@@ -14000,19 +13581,6 @@ click = ">=8.2.1"
|
||||
rich = ">=12.3.0"
|
||||
shellingham = ">=1.3.0"
|
||||
|
||||
[[package]]
|
||||
name = "types-certifi"
|
||||
version = "2021.10.8.3"
|
||||
description = "Typing stubs for certifi"
|
||||
optional = true
|
||||
python-versions = "*"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f"},
|
||||
{file = "types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "types-setuptools"
|
||||
version = "82.0.0.20260402"
|
||||
@@ -14305,22 +13873,6 @@ files = [
|
||||
[package.dependencies]
|
||||
anyio = ">=3.0.0"
|
||||
|
||||
[[package]]
|
||||
name = "wcmatch"
|
||||
version = "10.1"
|
||||
description = "Wildcard/glob file name matcher."
|
||||
optional = true
|
||||
python-versions = ">=3.9"
|
||||
groups = ["main"]
|
||||
markers = "extra == \"third-party-runtimes\""
|
||||
files = [
|
||||
{file = "wcmatch-10.1-py3-none-any.whl", hash = "sha256:5848ace7dbb0476e5e55ab63c6bbd529745089343427caa5537f230cc01beb8a"},
|
||||
{file = "wcmatch-10.1.tar.gz", hash = "sha256:f11f94208c8c8484a16f4f48638a85d771d9513f4ab3f37595978801cb9465af"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
bracex = ">=2.1.1"
|
||||
|
||||
[[package]]
|
||||
name = "wcwidth"
|
||||
version = "0.6.0"
|
||||
@@ -15031,12 +14583,9 @@ files = [
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and python_version < \"3.14\"", "cffi (>=2.0.0b0) ; platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""]
|
||||
|
||||
[extras]
|
||||
third-party-runtimes = ["daytona", "e2b-code-interpreter", "modal", "runloop-api-client"]
|
||||
cffi = ["cffi (>=1.17,<2.0) ; platform_python_implementation != \"PyPy\" and python_version < \"3.14\"", "cffi (>=2.0.0b) ; platform_python_implementation != \"PyPy\" and python_version >= \"3.14\""]
|
||||
|
||||
[metadata]
|
||||
lock-version = "2.1"
|
||||
python-versions = "^3.12,<3.14"
|
||||
content-hash = "5a44c35846ce89be68879be5a79c04faead517f7408f55d7dec8efcf6c8522a7"
|
||||
content-hash = "d0173b5b9cba1727e1d8ff19e49c08945d2920896f1a96ed7da450c3b9e7fb87"
|
||||
|
||||
@@ -106,12 +106,6 @@ dependencies = [
|
||||
"zope-interface==7.2",
|
||||
]
|
||||
|
||||
optional-dependencies.third_party_runtimes = [
|
||||
"daytona==0.24.2",
|
||||
"e2b-code-interpreter>=2",
|
||||
"modal>=0.66.26,<1.2",
|
||||
"runloop-api-client==0.50",
|
||||
]
|
||||
urls.Homepage = "https://github.com/OpenHands/OpenHands"
|
||||
urls.Repository = "https://github.com/OpenHands/OpenHands"
|
||||
|
||||
@@ -153,7 +147,6 @@ readme = "README.md"
|
||||
repository = "https://github.com/OpenHands/OpenHands"
|
||||
packages = [
|
||||
{ include = "openhands/**/*" },
|
||||
{ include = "third_party/**/*" },
|
||||
{ include = "pyproject.toml", to = "openhands" },
|
||||
{ include = "poetry.lock", to = "openhands" },
|
||||
]
|
||||
@@ -261,9 +254,6 @@ asyncpg = ">=0.30,<0.32"
|
||||
deprecation = "^2.1.0"
|
||||
lmnr = "^0.7.20"
|
||||
|
||||
[tool.poetry.extras]
|
||||
third_party_runtimes = [ "e2b-code-interpreter", "modal", "runloop-api-client", "daytona" ]
|
||||
|
||||
[tool.poetry.group.dev]
|
||||
optional = true
|
||||
|
||||
|
||||
@@ -44,75 +44,6 @@ def test_get_runtime_cls_works():
|
||||
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
|
||||
|
||||
Vendored
-14
@@ -1,14 +0,0 @@
|
||||
"""Third-party runtime implementations for OpenHands.
|
||||
|
||||
This module contains runtime implementations provided by third-party vendors.
|
||||
These runtimes are optional and require additional dependencies to be installed.
|
||||
|
||||
To use third-party runtimes, install OpenHands with the third_party_runtimes extra:
|
||||
pip install openhands-ai[third_party_runtimes]
|
||||
|
||||
Available third-party runtimes:
|
||||
- daytona: Daytona cloud development environment
|
||||
- e2b: E2B secure sandbox environment
|
||||
- modal: Modal cloud compute platform
|
||||
- runloop: Runloop AI sandbox environment
|
||||
"""
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
FROM ubuntu:24.04
|
||||
|
||||
# install basic packages
|
||||
RUN apt-get update && apt-get install -y \
|
||||
curl \
|
||||
wget \
|
||||
git \
|
||||
vim \
|
||||
nano \
|
||||
unzip \
|
||||
zip \
|
||||
python3 \
|
||||
python3-pip \
|
||||
python3-venv \
|
||||
python3-dev \
|
||||
build-essential \
|
||||
openssh-server \
|
||||
sudo \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
# How to build custom E2B sandbox for OpenHands
|
||||
|
||||
[E2B](https://e2b.dev) is an [open-source](https://github.com/e2b-dev/e2b) secure cloud environment (sandbox) made for running AI-generated code and agents. E2B offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b) SDK to spawn and control these sandboxes.
|
||||
|
||||
|
||||
1. Install the CLI with NPM.
|
||||
```sh
|
||||
npm install -g @e2b/cli@latest
|
||||
```
|
||||
Full CLI API is [here](https://e2b.dev/docs/cli/installation).
|
||||
|
||||
1. Build the sandbox
|
||||
```sh
|
||||
e2b template build --dockerfile ./Dockerfile --name "openhands"
|
||||
```
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
# This is a config for E2B sandbox template.
|
||||
# You can use 'template_id' (785n69crgahmz0lkdw9h) or 'template_name (openhands) from this config to spawn a sandbox:
|
||||
|
||||
# Python SDK
|
||||
# from e2b import Sandbox
|
||||
# sandbox = Sandbox(template='openhands')
|
||||
|
||||
# JS SDK
|
||||
# import { Sandbox } from 'e2b'
|
||||
# const sandbox = await Sandbox.create({ template: 'openhands' })
|
||||
|
||||
dockerfile = "Dockerfile"
|
||||
template_name = "openhands"
|
||||
template_id = "785n69crgahmz0lkdw9h"
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
"""Third-party runtime implementations."""
|
||||
Vendored
-1
@@ -1 +0,0 @@
|
||||
"""Third-party runtime implementation modules."""
|
||||
-134
@@ -1,134 +0,0 @@
|
||||
# Daytona Runtime
|
||||
|
||||
[Daytona](https://www.daytona.io/) is a platform that provides a secure and elastic infrastructure for running AI-generated code. It provides all the necessary features for an AI Agent to interact with a codebase. It provides a Daytona SDK with official Python and TypeScript interfaces for interacting with Daytona, enabling you to programmatically manage development environments and execute code.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Retrieve Your Daytona API Key
|
||||
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
|
||||
2. Click **"Create Key"**.
|
||||
3. Enter a name for your key and confirm the creation.
|
||||
4. Once the key is generated, copy it.
|
||||
|
||||
### Step 2: Set Your API Key as an Environment Variable
|
||||
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
|
||||
|
||||
Mac/Linux:
|
||||
```bash
|
||||
export DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
```powershell
|
||||
$env:DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
This step ensures that OpenHands can authenticate with the Daytona platform when it runs.
|
||||
|
||||
### Step 3: Run OpenHands Locally Using Docker
|
||||
To start the latest version of OpenHands on your machine, execute the following command in your terminal:
|
||||
|
||||
Mac/Linux:
|
||||
```bash
|
||||
bash -i <(curl -sL https://get.daytona.io/openhands)
|
||||
```
|
||||
|
||||
Windows:
|
||||
```powershell
|
||||
powershell -Command "irm https://get.daytona.io/openhands-windows | iex"
|
||||
```
|
||||
|
||||
#### What This Command Does:
|
||||
- Downloads the latest OpenHands release script.
|
||||
- Runs the script in an interactive Bash session.
|
||||
- Automatically pulls and runs the OpenHands container using Docker.
|
||||
Once executed, OpenHands should be running locally and ready for use.
|
||||
|
||||
|
||||
## Manual Initialization
|
||||
|
||||
### Step 1: Set the `OPENHANDS_VERSION` Environment Variable
|
||||
Run the following command in your terminal, replacing `<openhands-release>` with the latest release's version seen in the [main README.md file](https://github.com/OpenHands/OpenHands?tab=readme-ov-file#-quick-start):
|
||||
|
||||
#### Mac/Linux:
|
||||
```bash
|
||||
export OPENHANDS_VERSION="<openhands-release>" # e.g. 0.27
|
||||
```
|
||||
|
||||
#### Windows PowerShell:
|
||||
```powershell
|
||||
$env:OPENHANDS_VERSION="<openhands-release>" # e.g. 0.27
|
||||
```
|
||||
|
||||
### Step 2: Retrieve Your Daytona API Key
|
||||
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
|
||||
2. Click **"Create Key"**.
|
||||
3. Enter a name for your key and confirm the creation.
|
||||
4. Once the key is generated, copy it.
|
||||
|
||||
### Step 3: Set Your API Key as an Environment Variable:
|
||||
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
|
||||
|
||||
#### Mac/Linux:
|
||||
```bash
|
||||
export DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
#### Windows PowerShell:
|
||||
```powershell
|
||||
$env:DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
### Step 4: Run the following `docker` command:
|
||||
This command pulls and runs the OpenHands container using Docker. Once executed, OpenHands should be running locally and ready for use.
|
||||
|
||||
#### Mac/Linux:
|
||||
```bash
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:${OPENHANDS_VERSION}-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-e RUNTIME=daytona \
|
||||
-e DAYTONA_API_KEY=${DAYTONA_API_KEY} \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--name openhands-app \
|
||||
docker.openhands.dev/openhands/openhands:${OPENHANDS_VERSION}
|
||||
```
|
||||
|
||||
> **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.
|
||||
|
||||
#### Windows:
|
||||
```powershell
|
||||
docker run -it --rm --pull=always `
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.openhands.dev/openhands/runtime:${env:OPENHANDS_VERSION}-nikolaik `
|
||||
-e LOG_ALL_EVENTS=true `
|
||||
-e RUNTIME=daytona `
|
||||
-e DAYTONA_API_KEY=${env:DAYTONA_API_KEY} `
|
||||
-v ~/.openhands:/.openhands `
|
||||
-p 3000:3000 `
|
||||
--name openhands-app `
|
||||
docker.openhands.dev/openhands/openhands:${env:OPENHANDS_VERSION}
|
||||
```
|
||||
|
||||
> **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.
|
||||
|
||||
> **Tip:** If you don't want your sandboxes to default to the EU region, you can set the `DAYTONA_TARGET` environment variable to `us`
|
||||
|
||||
### Running OpenHands Locally Without Docker
|
||||
|
||||
Alternatively, if you want to run the OpenHands app on your local machine using `make run` without Docker, make sure to set the following environment variables first:
|
||||
|
||||
#### Mac/Linux:
|
||||
```bash
|
||||
export RUNTIME="daytona"
|
||||
export DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
#### Windows PowerShell:
|
||||
```powershell
|
||||
$env:RUNTIME="daytona"
|
||||
$env:DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Read more by visiting our [documentation](https://www.daytona.io/docs/) page.
|
||||
@@ -1,7 +0,0 @@
|
||||
"""Daytona runtime implementation.
|
||||
|
||||
This runtime reads configuration directly from environment variables:
|
||||
- DAYTONA_API_KEY: API key for Daytona authentication
|
||||
- DAYTONA_API_URL: Daytona API URL endpoint (defaults to https://app.daytona.io/api)
|
||||
- DAYTONA_TARGET: Daytona target region (defaults to 'eu')
|
||||
"""
|
||||
-297
@@ -1,297 +0,0 @@
|
||||
import os
|
||||
from typing import Callable
|
||||
|
||||
import httpx
|
||||
import tenacity
|
||||
from daytona import (
|
||||
CreateSandboxFromSnapshotParams,
|
||||
Daytona,
|
||||
DaytonaConfig,
|
||||
Sandbox,
|
||||
SessionExecuteRequest,
|
||||
)
|
||||
|
||||
from openhands.core.config.openhands_config import OpenHandsConfig
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient,
|
||||
)
|
||||
from openhands.runtime.plugins.requirement import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.command import get_action_execution_server_startup_command
|
||||
from openhands.runtime.utils.request import RequestHTTPError
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
from openhands.utils.tenacity_stop import stop_if_should_exit
|
||||
|
||||
OPENHANDS_SID_LABEL = "OpenHands_SID"
|
||||
|
||||
|
||||
class DaytonaRuntime(ActionExecutionClient):
|
||||
"""The DaytonaRuntime class is a DockerRuntime that utilizes Daytona Sandboxes as runtime environments."""
|
||||
|
||||
_sandbox_port: int = 4444
|
||||
_vscode_port: int = 4445
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: OpenHandsConfig,
|
||||
event_stream: EventStream,
|
||||
sid: str = "default",
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
attach_to_existing: bool = False,
|
||||
headless_mode: bool = True,
|
||||
user_id: str | None = None,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
|
||||
):
|
||||
# Read Daytona configuration from environment variables
|
||||
daytona_api_key = os.getenv("DAYTONA_API_KEY")
|
||||
if not daytona_api_key:
|
||||
raise ValueError(
|
||||
"DAYTONA_API_KEY environment variable is required for Daytona runtime"
|
||||
)
|
||||
daytona_api_url = os.getenv("DAYTONA_API_URL", "https://app.daytona.io/api")
|
||||
daytona_target = os.getenv("DAYTONA_TARGET", "eu")
|
||||
|
||||
self.config = config
|
||||
self.sid = sid
|
||||
self.sandbox: Sandbox | None = None
|
||||
self._vscode_url: str | None = None
|
||||
|
||||
daytona_config = DaytonaConfig(
|
||||
api_key=daytona_api_key,
|
||||
server_url=daytona_api_url,
|
||||
target=daytona_target,
|
||||
)
|
||||
self.daytona = Daytona(daytona_config)
|
||||
|
||||
# workspace_base cannot be used because we can't bind mount into a workspace.
|
||||
if self.config.workspace_base is not None:
|
||||
self.log(
|
||||
"warning",
|
||||
"Workspace mounting is not supported in the Daytona runtime.",
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
event_stream,
|
||||
sid,
|
||||
plugins,
|
||||
env_vars,
|
||||
status_callback,
|
||||
attach_to_existing,
|
||||
headless_mode,
|
||||
user_id,
|
||||
git_provider_tokens,
|
||||
)
|
||||
|
||||
def _get_sandbox(self) -> Sandbox | None:
|
||||
try:
|
||||
sandboxes = self.daytona.list({OPENHANDS_SID_LABEL: self.sid})
|
||||
if len(sandboxes) == 0:
|
||||
return None
|
||||
assert len(sandboxes) == 1, "Multiple sandboxes found for SID"
|
||||
|
||||
sandbox = sandboxes[0]
|
||||
|
||||
self.log("info", f"Attached to existing sandbox with id: {self.sid}")
|
||||
except Exception:
|
||||
self.log(
|
||||
"warning",
|
||||
f"Failed to attach to existing sandbox with id: {self.sid}",
|
||||
)
|
||||
sandbox = None
|
||||
|
||||
return sandbox
|
||||
|
||||
def _get_creation_env_vars(self) -> dict[str, str]:
|
||||
env_vars: dict[str, str] = {
|
||||
"port": str(self._sandbox_port),
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"VSCODE_PORT": str(self._vscode_port),
|
||||
}
|
||||
|
||||
if self.config.debug:
|
||||
env_vars["DEBUG"] = "true"
|
||||
|
||||
return env_vars
|
||||
|
||||
def _create_sandbox(self) -> Sandbox:
|
||||
# Check if auto-stop should be disabled - otherwise have it trigger after 60 minutes
|
||||
disable_auto_stop = (
|
||||
os.getenv("DAYTONA_DISABLE_AUTO_STOP", "false").lower() == "true"
|
||||
)
|
||||
auto_stop_interval = 0 if disable_auto_stop else 60
|
||||
|
||||
sandbox_params = CreateSandboxFromSnapshotParams(
|
||||
language="python",
|
||||
snapshot=self.config.sandbox.runtime_container_image,
|
||||
public=True,
|
||||
env_vars=self._get_creation_env_vars(),
|
||||
labels={OPENHANDS_SID_LABEL: self.sid},
|
||||
auto_stop_interval=auto_stop_interval,
|
||||
)
|
||||
return self.daytona.create(sandbox_params)
|
||||
|
||||
def _construct_api_url(self, port: int) -> str:
|
||||
assert self.sandbox is not None, "Sandbox is not initialized"
|
||||
return self.sandbox.get_preview_link(port).url
|
||||
|
||||
@property
|
||||
def action_execution_server_url(self) -> str:
|
||||
return self.api_url
|
||||
|
||||
def _start_action_execution_server(self) -> None:
|
||||
assert self.sandbox is not None, "Sandbox is not initialized"
|
||||
|
||||
start_command: list[str] = get_action_execution_server_startup_command(
|
||||
server_port=self._sandbox_port,
|
||||
plugins=self.plugins,
|
||||
app_config=self.config,
|
||||
override_user_id=1000,
|
||||
override_username="openhands",
|
||||
)
|
||||
start_command_str: str = (
|
||||
f"mkdir -p {self.config.workspace_mount_path_in_sandbox} && cd /openhands/code && "
|
||||
+ " ".join(start_command)
|
||||
)
|
||||
|
||||
self.log(
|
||||
"debug",
|
||||
f"Starting action execution server with command: {start_command_str}",
|
||||
)
|
||||
|
||||
exec_session_id = "action-execution-server"
|
||||
self.sandbox.process.create_session(exec_session_id)
|
||||
|
||||
exec_command = self.sandbox.process.execute_session_command(
|
||||
exec_session_id,
|
||||
SessionExecuteRequest(command=start_command_str, var_async=True),
|
||||
)
|
||||
|
||||
self.log("debug", f"exec_command_id: {exec_command.cmd_id}")
|
||||
|
||||
@tenacity.retry(
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
wait=tenacity.wait_fixed(1),
|
||||
reraise=(ConnectionRefusedError,),
|
||||
)
|
||||
def _wait_until_alive(self):
|
||||
super().check_if_alive()
|
||||
|
||||
async def connect(self):
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
should_start_action_execution_server = False
|
||||
|
||||
if self.attach_to_existing:
|
||||
self.sandbox = await call_sync_from_async(self._get_sandbox)
|
||||
else:
|
||||
should_start_action_execution_server = True
|
||||
|
||||
if self.sandbox is None:
|
||||
self.set_runtime_status(RuntimeStatus.BUILDING_RUNTIME)
|
||||
self.sandbox = await call_sync_from_async(self._create_sandbox)
|
||||
self.log("info", f"Created a new sandbox with id: {self.sid}")
|
||||
|
||||
self.api_url = self._construct_api_url(self._sandbox_port)
|
||||
|
||||
state = self.sandbox.state
|
||||
|
||||
if state == "stopping":
|
||||
self.log("info", "Waiting for the Daytona sandbox to stop...")
|
||||
await call_sync_from_async(self.sandbox.wait_for_sandbox_stop)
|
||||
state = "stopped"
|
||||
|
||||
if state == "stopped":
|
||||
self.log("info", "Starting the Daytona sandbox...")
|
||||
await call_sync_from_async(self.sandbox.start)
|
||||
should_start_action_execution_server = True
|
||||
|
||||
if should_start_action_execution_server:
|
||||
await call_sync_from_async(self._start_action_execution_server)
|
||||
self.log(
|
||||
"info",
|
||||
f"Container started. Action execution server url: {self.api_url}",
|
||||
)
|
||||
|
||||
self.log("info", "Waiting for client to become ready...")
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
await call_sync_from_async(self._wait_until_alive)
|
||||
|
||||
if should_start_action_execution_server:
|
||||
await call_sync_from_async(self.setup_initial_env)
|
||||
|
||||
self.log(
|
||||
"info",
|
||||
f"Container initialized with plugins: {[plugin.name for plugin in self.plugins]}",
|
||||
)
|
||||
|
||||
if should_start_action_execution_server:
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self._runtime_initialized = True
|
||||
|
||||
@tenacity.retry(
|
||||
retry=tenacity.retry_if_exception(
|
||||
lambda e: (
|
||||
isinstance(e, httpx.HTTPError) or isinstance(e, RequestHTTPError)
|
||||
)
|
||||
and hasattr(e, "response")
|
||||
and e.response.status_code == 502
|
||||
),
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
wait=tenacity.wait_fixed(1),
|
||||
reraise=True,
|
||||
)
|
||||
def _send_action_server_request(self, method, url, **kwargs):
|
||||
return super()._send_action_server_request(method, url, **kwargs)
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
|
||||
if self.attach_to_existing:
|
||||
return
|
||||
|
||||
if self.sandbox:
|
||||
delete_on_close = (
|
||||
os.getenv("DAYTONA_DELETE_ON_CLOSE", "false").lower() == "true"
|
||||
)
|
||||
|
||||
if delete_on_close:
|
||||
self.sandbox.delete()
|
||||
else:
|
||||
# Only stop if sandbox is currently started
|
||||
if self._get_sandbox().state == "started":
|
||||
self.sandbox.stop()
|
||||
|
||||
@property
|
||||
def vscode_url(self) -> str | None:
|
||||
if self._vscode_url is not None: # cached value
|
||||
return self._vscode_url
|
||||
token = super().get_vscode_token()
|
||||
if not token:
|
||||
self.log(
|
||||
"warning", "Failed to get VSCode token while trying to get VSCode URL"
|
||||
)
|
||||
return None
|
||||
if not self.sandbox:
|
||||
self.log(
|
||||
"warning", "Sandbox is not initialized while trying to get VSCode URL"
|
||||
)
|
||||
return None
|
||||
self._vscode_url = (
|
||||
self._construct_api_url(self._vscode_port)
|
||||
+ f"/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}"
|
||||
)
|
||||
|
||||
self.log(
|
||||
"debug",
|
||||
f"VSCode URL: {self._vscode_url}",
|
||||
)
|
||||
|
||||
return self._vscode_url
|
||||
|
||||
@property
|
||||
def additional_agent_instructions(self) -> str:
|
||||
return f"When showing endpoints to access applications for any port, e.g. port 3000, instead of localhost:3000, use this format: {self._construct_api_url(3000)}."
|
||||
-35
@@ -1,35 +0,0 @@
|
||||
# How to use E2B
|
||||
|
||||
[E2B](https://e2b.dev) is an [open-source](https://github.com/e2b-dev/e2b) secure cloud environment (sandbox) made for running AI-generated code and agents. E2B offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b) SDK to spawn and control these sandboxes.
|
||||
|
||||
## Getting started
|
||||
|
||||
1. [Get your API key](https://e2b.dev/docs/getting-started/api-key)
|
||||
|
||||
1. Set your E2B API key to the `E2B_API_KEY` env var when starting the Docker container
|
||||
|
||||
1. **Optional** - Install the CLI with NPM.
|
||||
```sh
|
||||
npm install -g @e2b/cli@latest
|
||||
```
|
||||
Full CLI API is [here](https://e2b.dev/docs/cli/installation).
|
||||
|
||||
## OpenHands sandbox
|
||||
You can use the E2B CLI to create a custom sandbox with a Dockerfile. Read the full guide [here](https://e2b.dev/docs/guide/custom-sandbox). The premade OpenHands sandbox for E2B is set up in the [`containers` directory](/containers/e2b-sandbox). and it's called `openhands`.
|
||||
|
||||
## Debugging
|
||||
You can connect to a running E2B sandbox with E2B CLI in your terminal.
|
||||
|
||||
- List all running sandboxes (based on your API key)
|
||||
```sh
|
||||
e2b sandbox list
|
||||
```
|
||||
|
||||
- Connect to a running sandbox
|
||||
```sh
|
||||
e2b sandbox connect <sandbox-id>
|
||||
```
|
||||
|
||||
## Links
|
||||
- [E2B Docs](https://e2b.dev/docs)
|
||||
- [E2B GitHub](https://github.com/e2b-dev/e2b)
|
||||
-5
@@ -1,5 +0,0 @@
|
||||
"""E2B runtime implementation.
|
||||
|
||||
This runtime reads configuration directly from environment variables:
|
||||
- E2B_API_KEY: API key for E2B authentication
|
||||
"""
|
||||
-402
@@ -1,402 +0,0 @@
|
||||
import os
|
||||
from typing import Callable
|
||||
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events.action import (
|
||||
BrowseURLAction,
|
||||
BrowseInteractiveAction,
|
||||
CmdRunAction,
|
||||
FileEditAction,
|
||||
FileReadAction,
|
||||
FileWriteAction,
|
||||
IPythonRunCellAction,
|
||||
)
|
||||
from openhands.events.observation import (
|
||||
BrowserOutputObservation,
|
||||
CmdOutputObservation,
|
||||
ErrorObservation,
|
||||
FileEditObservation,
|
||||
FileReadObservation,
|
||||
FileWriteObservation,
|
||||
IPythonRunCellObservation,
|
||||
Observation,
|
||||
)
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.llm.llm_registry import LLMRegistry
|
||||
from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient,
|
||||
)
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.files import insert_lines, read_lines
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
from third_party.runtime.impl.e2b.filestore import E2BFileStore
|
||||
from third_party.runtime.impl.e2b.sandbox import E2BBox, E2BSandbox
|
||||
|
||||
|
||||
class E2BRuntime(ActionExecutionClient):
|
||||
# Class-level cache for sandbox IDs
|
||||
_sandbox_id_cache: dict[str, str] = {}
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: OpenHandsConfig,
|
||||
event_stream: EventStream,
|
||||
llm_registry: LLMRegistry,
|
||||
sid: str = "default",
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
attach_to_existing: bool = False,
|
||||
headless_mode: bool = True,
|
||||
user_id: str | None = None,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
|
||||
sandbox: E2BSandbox | None = None,
|
||||
):
|
||||
if config.workspace_base is not None:
|
||||
logger.warning(
|
||||
"Setting workspace_base is not supported in the E2B runtime. "
|
||||
"E2B provides its own isolated filesystem."
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
config=config,
|
||||
event_stream=event_stream,
|
||||
llm_registry=llm_registry,
|
||||
sid=sid,
|
||||
plugins=plugins,
|
||||
env_vars=env_vars,
|
||||
status_callback=status_callback,
|
||||
attach_to_existing=attach_to_existing,
|
||||
headless_mode=headless_mode,
|
||||
user_id=user_id,
|
||||
git_provider_tokens=git_provider_tokens,
|
||||
)
|
||||
self.sandbox = sandbox
|
||||
self.file_store = None
|
||||
self.api_url = None
|
||||
self._action_server_port = 8000
|
||||
self._runtime_initialized = False
|
||||
|
||||
@property
|
||||
def action_execution_server_url(self) -> str:
|
||||
"""Return the URL of the action execution server."""
|
||||
if not self.api_url:
|
||||
raise RuntimeError("E2B runtime not connected. Call connect() first.")
|
||||
return self.api_url
|
||||
|
||||
async def connect(self) -> None:
|
||||
"""Initialize E2B sandbox and start action execution server."""
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
try:
|
||||
if self.attach_to_existing and self.sandbox is None:
|
||||
try:
|
||||
cached_sandbox_id = self.__class__._sandbox_id_cache.get(self.sid)
|
||||
|
||||
if cached_sandbox_id:
|
||||
try:
|
||||
self.sandbox = E2BBox(self.config.sandbox, sandbox_id=cached_sandbox_id)
|
||||
logger.info(f"Successfully attached to existing E2B sandbox: {cached_sandbox_id}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to connect to cached sandbox {cached_sandbox_id}: {e}")
|
||||
del self.__class__._sandbox_id_cache[self.sid]
|
||||
self.sandbox = None
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to attach to existing sandbox: {e}. Will create a new one.")
|
||||
|
||||
# Create E2B sandbox if not provided
|
||||
if self.sandbox is None:
|
||||
try:
|
||||
self.sandbox = E2BSandbox(self.config.sandbox)
|
||||
sandbox_id = self.sandbox.sandbox.sandbox_id
|
||||
logger.info(f"E2B sandbox created with ID: {sandbox_id}")
|
||||
|
||||
self.__class__._sandbox_id_cache[self.sid] = sandbox_id
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create E2B sandbox: {e}")
|
||||
raise
|
||||
|
||||
if not isinstance(self.sandbox, (E2BSandbox, E2BBox)):
|
||||
raise ValueError("E2BRuntime requires an E2BSandbox or E2BBox")
|
||||
|
||||
self.file_store = E2BFileStore(self.sandbox.filesystem)
|
||||
|
||||
# E2B doesn't use action execution server - set dummy URL
|
||||
self.api_url = "direct://e2b-sandbox"
|
||||
|
||||
workspace_dir = self.config.workspace_mount_path_in_sandbox
|
||||
if workspace_dir:
|
||||
try:
|
||||
exit_code, output = self.sandbox.execute(f"sudo mkdir -p {workspace_dir}")
|
||||
if exit_code == 0:
|
||||
self.sandbox.execute(f"sudo chmod 777 {workspace_dir}")
|
||||
logger.info(f"Created workspace directory: {workspace_dir}")
|
||||
else:
|
||||
logger.warning(f"Failed to create workspace directory: {output}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to create workspace directory: {e}")
|
||||
|
||||
await call_sync_from_async(self.setup_initial_env)
|
||||
|
||||
self._runtime_initialized = True
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
logger.info("E2B runtime connected successfully")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to connect E2B runtime: {e}")
|
||||
self.set_runtime_status(RuntimeStatus.FAILED)
|
||||
raise
|
||||
|
||||
async def close(self) -> None:
|
||||
"""Close the E2B runtime."""
|
||||
if self._runtime_closed:
|
||||
return
|
||||
|
||||
self._runtime_closed = True
|
||||
|
||||
if self.sandbox:
|
||||
try:
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.sandbox.close()
|
||||
if self.sid in self.__class__._sandbox_id_cache:
|
||||
del self.__class__._sandbox_id_cache[self.sid]
|
||||
logger.info("E2B sandbox closed and removed from cache")
|
||||
else:
|
||||
logger.info("E2B runtime connection closed, sandbox kept running for reuse")
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error closing E2B sandbox: {e}")
|
||||
|
||||
parent_close = super().close()
|
||||
if parent_close is not None:
|
||||
await parent_close
|
||||
|
||||
def run(self, action: CmdRunAction) -> Observation:
|
||||
"""Execute command using E2B's native execute method."""
|
||||
if self.sandbox is None:
|
||||
return ErrorObservation("E2B sandbox not initialized")
|
||||
|
||||
try:
|
||||
timeout = action.timeout if action.timeout else self.config.sandbox.timeout
|
||||
exit_code, output = self.sandbox.execute(action.command, timeout=timeout)
|
||||
return CmdOutputObservation(
|
||||
content=output,
|
||||
command=action.command,
|
||||
exit_code=exit_code
|
||||
)
|
||||
except Exception as e:
|
||||
return ErrorObservation(f"Failed to execute command: {e}")
|
||||
|
||||
def run_ipython(self, action: IPythonRunCellAction) -> Observation:
|
||||
"""Execute IPython code using E2B's code interpreter."""
|
||||
if self.sandbox is None:
|
||||
return ErrorObservation("E2B sandbox not initialized")
|
||||
|
||||
try:
|
||||
result = self.sandbox.sandbox.run_code(action.code)
|
||||
|
||||
outputs = []
|
||||
if hasattr(result, 'results') and result.results:
|
||||
for r in result.results:
|
||||
if hasattr(r, 'text') and r.text:
|
||||
outputs.append(r.text)
|
||||
elif hasattr(r, 'html') and r.html:
|
||||
outputs.append(r.html)
|
||||
elif hasattr(r, 'png') and r.png:
|
||||
outputs.append(f"[Image data: {len(r.png)} bytes]")
|
||||
|
||||
if hasattr(result, 'error') and result.error:
|
||||
return ErrorObservation(f"IPython error: {result.error}")
|
||||
|
||||
return IPythonRunCellObservation(
|
||||
content='\n'.join(outputs) if outputs else '',
|
||||
code=action.code
|
||||
)
|
||||
except Exception as e:
|
||||
return ErrorObservation(f"Failed to execute IPython code: {e}")
|
||||
|
||||
def read(self, action: FileReadAction) -> Observation:
|
||||
if self.file_store is None:
|
||||
return ErrorObservation("E2B file store not initialized. Call connect() first.")
|
||||
|
||||
try:
|
||||
content = self.file_store.read(action.path)
|
||||
lines = read_lines(content.split("\n"), action.start, action.end)
|
||||
code_view = "".join(lines)
|
||||
return FileReadObservation(code_view, path=action.path)
|
||||
except Exception as e:
|
||||
return ErrorObservation(f"Failed to read file: {e}")
|
||||
|
||||
def write(self, action: FileWriteAction) -> Observation:
|
||||
if self.file_store is None:
|
||||
return ErrorObservation("E2B file store not initialized. Call connect() first.")
|
||||
|
||||
try:
|
||||
if action.start == 0 and action.end == -1:
|
||||
self.file_store.write(action.path, action.content)
|
||||
return FileWriteObservation(content="", path=action.path)
|
||||
|
||||
files = self.file_store.list(action.path)
|
||||
if action.path in files:
|
||||
all_lines = self.file_store.read(action.path).split("\n")
|
||||
new_file = insert_lines(
|
||||
action.content.split("\n"), all_lines, action.start, action.end
|
||||
)
|
||||
self.file_store.write(action.path, "".join(new_file))
|
||||
return FileWriteObservation("", path=action.path)
|
||||
else:
|
||||
# Create a new file
|
||||
self.file_store.write(action.path, action.content)
|
||||
return FileWriteObservation(content="", path=action.path)
|
||||
except Exception as e:
|
||||
return ErrorObservation(f"Failed to write file: {e}")
|
||||
|
||||
def edit(self, action: FileEditAction) -> Observation:
|
||||
"""Edit a file using E2B's file system."""
|
||||
if self.file_store is None:
|
||||
return ErrorObservation("E2B file store not initialized. Call connect() first.")
|
||||
|
||||
try:
|
||||
if action.path in self.file_store.list(action.path):
|
||||
content = self.file_store.read(action.path)
|
||||
else:
|
||||
return ErrorObservation(f"File {action.path} not found")
|
||||
|
||||
lines = content.split('\n')
|
||||
if action.start < 0 or action.end > len(lines):
|
||||
return ErrorObservation(f"Invalid line range: {action.start}-{action.end}")
|
||||
|
||||
new_lines = lines[:action.start] + action.content.split('\n') + lines[action.end:]
|
||||
new_content = '\n'.join(new_lines)
|
||||
|
||||
self.file_store.write(action.path, new_content)
|
||||
|
||||
return FileEditObservation(
|
||||
content='',
|
||||
path=action.path,
|
||||
old_content='\n'.join(lines[action.start:action.end]),
|
||||
start=action.start,
|
||||
end=action.end
|
||||
)
|
||||
except Exception as e:
|
||||
return ErrorObservation(f"Failed to edit file: {e}")
|
||||
|
||||
def browse(self, action: BrowseURLAction) -> Observation:
|
||||
"""Browse a URL using E2B's browser capabilities."""
|
||||
if self.sandbox is None:
|
||||
return ErrorObservation("E2B sandbox not initialized")
|
||||
|
||||
try:
|
||||
exit_code, output = self.sandbox.execute(f"curl -s -L '{action.url}'")
|
||||
if exit_code != 0:
|
||||
exit_code, output = self.sandbox.execute(f"wget -qO- '{action.url}'")
|
||||
|
||||
if exit_code != 0:
|
||||
return ErrorObservation(f"Failed to fetch URL: {output}")
|
||||
|
||||
return BrowserOutputObservation(
|
||||
content=output,
|
||||
url=action.url,
|
||||
screenshot=None,
|
||||
error=None if exit_code == 0 else output
|
||||
)
|
||||
except Exception as e:
|
||||
return ErrorObservation(f"Failed to browse URL: {e}")
|
||||
|
||||
def browse_interactive(self, action: BrowseInteractiveAction) -> Observation:
|
||||
"""Interactive browsing is not supported in E2B."""
|
||||
return ErrorObservation(
|
||||
"Interactive browsing is not supported in E2B runtime. "
|
||||
"Use browse() for simple URL fetching or consider using a different runtime."
|
||||
)
|
||||
|
||||
def list_files(self, path: str | None = None) -> list[str]:
|
||||
"""List files in the sandbox."""
|
||||
if self.sandbox is None:
|
||||
logger.warning("Cannot list files: E2B sandbox not initialized")
|
||||
return []
|
||||
|
||||
if path is None:
|
||||
path = self.config.workspace_mount_path_in_sandbox or '/workspace'
|
||||
|
||||
try:
|
||||
exit_code, output = self.sandbox.execute(f"find {path} -maxdepth 1 -type f -o -type d")
|
||||
if exit_code == 0:
|
||||
files = [line.strip() for line in output.strip().split('\n') if line.strip()]
|
||||
return [f.replace(path + '/', '') if f.startswith(path + '/') else f for f in files]
|
||||
else:
|
||||
logger.warning(f"Failed to list files in {path}: {output}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.warning(f"Error listing files: {e}")
|
||||
return []
|
||||
|
||||
def add_env_vars(self, env_vars: dict[str, str]) -> None:
|
||||
"""Add environment variables to the E2B sandbox."""
|
||||
if self.sandbox is None:
|
||||
logger.warning("Cannot add env vars: E2B sandbox not initialized")
|
||||
return
|
||||
|
||||
if not hasattr(self, '_env_vars'):
|
||||
self._env_vars = {}
|
||||
self._env_vars.update(env_vars)
|
||||
|
||||
for key, value in env_vars.items():
|
||||
try:
|
||||
escaped_value = value.replace("'", "'\"'\"'")
|
||||
cmd = f"export {key}='{escaped_value}'"
|
||||
self.sandbox.execute(cmd)
|
||||
logger.debug(f"Set env var: {key}")
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to set env var {key}: {e}")
|
||||
|
||||
def get_working_directory(self) -> str:
|
||||
"""Get the current working directory."""
|
||||
if self.sandbox is None:
|
||||
return self.config.workspace_mount_path_in_sandbox or '/workspace'
|
||||
try:
|
||||
exit_code, output = self.sandbox.execute("pwd")
|
||||
if exit_code == 0:
|
||||
return output.strip()
|
||||
except Exception:
|
||||
pass
|
||||
return self.config.workspace_mount_path_in_sandbox or '/workspace'
|
||||
|
||||
def get_mcp_config(self, extra_stdio_servers: list | None = None) -> dict:
|
||||
"""Get MCP configuration for E2B runtime."""
|
||||
return {
|
||||
'stdio_servers': extra_stdio_servers or []
|
||||
}
|
||||
|
||||
def check_if_alive(self) -> None:
|
||||
"""Check if the E2B sandbox is alive."""
|
||||
if self.sandbox is None:
|
||||
raise RuntimeError("E2B sandbox not initialized")
|
||||
return
|
||||
|
||||
def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False) -> None:
|
||||
"""Copy files to the E2B sandbox."""
|
||||
if self.sandbox is None:
|
||||
raise RuntimeError("E2B sandbox not initialized")
|
||||
self.sandbox.copy_to(host_src, sandbox_dest, recursive)
|
||||
|
||||
def get_vscode_token(self) -> str:
|
||||
"""E2B doesn't support VSCode integration."""
|
||||
return ""
|
||||
|
||||
@classmethod
|
||||
def setup(cls, config: OpenHandsConfig, headless_mode: bool = False) -> None:
|
||||
"""Set up the E2B runtime environment."""
|
||||
logger.info("E2B runtime setup called")
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def teardown(cls, config: OpenHandsConfig) -> None:
|
||||
"""Tear down the E2B runtime environment."""
|
||||
logger.info("E2B runtime teardown called")
|
||||
pass
|
||||
-27
@@ -1,27 +0,0 @@
|
||||
from typing import Protocol
|
||||
|
||||
from openhands.storage.files import FileStore
|
||||
|
||||
|
||||
class SupportsFilesystemOperations(Protocol):
|
||||
def write(self, path: str, contents: str | bytes) -> None: ...
|
||||
def read(self, path: str) -> str: ...
|
||||
def list(self, path: str) -> list[str]: ...
|
||||
def delete(self, path: str) -> None: ...
|
||||
|
||||
|
||||
class E2BFileStore(FileStore):
|
||||
def __init__(self, filesystem: SupportsFilesystemOperations) -> None:
|
||||
self.filesystem = filesystem
|
||||
|
||||
def write(self, path: str, contents: str | bytes) -> None:
|
||||
self.filesystem.write(path, contents)
|
||||
|
||||
def read(self, path: str) -> str:
|
||||
return self.filesystem.read(path)
|
||||
|
||||
def list(self, path: str) -> list[str]:
|
||||
return self.filesystem.list(path)
|
||||
|
||||
def delete(self, path: str) -> None:
|
||||
self.filesystem.delete(path)
|
||||
-152
@@ -1,152 +0,0 @@
|
||||
import copy
|
||||
import os
|
||||
import tarfile
|
||||
from glob import glob
|
||||
|
||||
from e2b_code_interpreter import Sandbox
|
||||
from e2b.exceptions import TimeoutException
|
||||
|
||||
from openhands.core.config import SandboxConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
|
||||
class E2BBox:
|
||||
closed = False
|
||||
_cwd: str = "/home/user"
|
||||
_env: dict[str, str] = {}
|
||||
is_initial_session: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: SandboxConfig,
|
||||
sandbox_id: str | None = None,
|
||||
):
|
||||
self.config = copy.deepcopy(config)
|
||||
self.initialize_plugins: bool = config.initialize_plugins
|
||||
|
||||
# Read API key from environment variable
|
||||
e2b_api_key = os.getenv("E2B_API_KEY")
|
||||
if not e2b_api_key:
|
||||
raise ValueError(
|
||||
"E2B_API_KEY environment variable is required for E2B runtime"
|
||||
)
|
||||
|
||||
# Read custom E2B domain if provided
|
||||
e2b_domain = os.getenv("E2B_DOMAIN")
|
||||
if e2b_domain:
|
||||
logger.info(f'Using custom E2B domain: {e2b_domain}')
|
||||
|
||||
# E2B v2 requires using create() method or connect to existing
|
||||
try:
|
||||
# Configure E2B client with custom domain if provided
|
||||
create_kwargs = {}
|
||||
connect_kwargs = {}
|
||||
|
||||
if e2b_domain:
|
||||
# Set up custom domain configuration
|
||||
# Note: This depends on E2B SDK version and may need adjustment
|
||||
os.environ['E2B_API_URL'] = f'https://{e2b_domain}'
|
||||
logger.info(f'Set E2B_API_URL to https://{e2b_domain}')
|
||||
|
||||
if sandbox_id:
|
||||
# Connect to existing sandbox
|
||||
self.sandbox = Sandbox.connect(sandbox_id, **connect_kwargs)
|
||||
logger.info(f'Connected to existing E2B sandbox with ID "{sandbox_id}"')
|
||||
else:
|
||||
# Create new sandbox (e2b-code-interpreter doesn't need template)
|
||||
self.sandbox = Sandbox.create(**create_kwargs)
|
||||
sandbox_id = self.sandbox.sandbox_id
|
||||
logger.info(f'Created E2B sandbox with ID "{sandbox_id}"')
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to create/connect E2B sandbox: {e}")
|
||||
raise
|
||||
|
||||
@property
|
||||
def filesystem(self):
|
||||
# E2B v2 uses 'files' instead of 'filesystem'
|
||||
return getattr(self.sandbox, 'files', None) or getattr(self.sandbox, 'filesystem', None)
|
||||
|
||||
def _archive(self, host_src: str, recursive: bool = False):
|
||||
if recursive:
|
||||
assert os.path.isdir(host_src), (
|
||||
"Source must be a directory when recursive is True"
|
||||
)
|
||||
files = glob(host_src + "/**/*", recursive=True)
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + ".tar")
|
||||
with tarfile.open(tar_filename, mode="w") as tar:
|
||||
for file in files:
|
||||
tar.add(
|
||||
file, arcname=os.path.relpath(file, os.path.dirname(host_src))
|
||||
)
|
||||
else:
|
||||
assert os.path.isfile(host_src), (
|
||||
"Source must be a file when recursive is False"
|
||||
)
|
||||
srcname = os.path.basename(host_src)
|
||||
tar_filename = os.path.join(os.path.dirname(host_src), srcname + ".tar")
|
||||
with tarfile.open(tar_filename, mode="w") as tar:
|
||||
tar.add(host_src, arcname=srcname)
|
||||
return tar_filename
|
||||
|
||||
def execute(self, cmd: str, timeout: int | None = None) -> tuple[int, str]:
|
||||
timeout = timeout if timeout is not None else self.config.timeout
|
||||
|
||||
# E2B code-interpreter uses commands.run()
|
||||
try:
|
||||
result = self.sandbox.commands.run(cmd)
|
||||
output = ""
|
||||
if hasattr(result, 'stdout') and result.stdout:
|
||||
output += result.stdout
|
||||
if hasattr(result, 'stderr') and result.stderr:
|
||||
output += result.stderr
|
||||
exit_code = getattr(result, 'exit_code', 0) or 0
|
||||
return exit_code, output
|
||||
except TimeoutException:
|
||||
logger.debug("Command timed out")
|
||||
return -1, f'Command: "{cmd}" timed out'
|
||||
except Exception as e:
|
||||
logger.error(f"Command execution failed: {e}")
|
||||
return -1, str(e)
|
||||
|
||||
def copy_to(self, host_src: str, sandbox_dest: str, recursive: bool = False):
|
||||
"""Copies a local file or directory to the sandbox."""
|
||||
tar_filename = self._archive(host_src, recursive)
|
||||
|
||||
# Prepend the sandbox destination with our sandbox cwd
|
||||
sandbox_dest = os.path.join(self._cwd, sandbox_dest.removeprefix("/"))
|
||||
|
||||
with open(tar_filename, "rb") as tar_file:
|
||||
# Upload the archive to /home/user (default destination that always exists)
|
||||
uploaded_path = self.sandbox.upload_file(tar_file)
|
||||
|
||||
# Check if sandbox_dest exists. If not, create it.
|
||||
exit_code, _ = self.execute(f"test -d {sandbox_dest}")
|
||||
if exit_code != 0:
|
||||
self.execute(f"mkdir -p {sandbox_dest}")
|
||||
|
||||
# Extract the archive into the destination and delete the archive
|
||||
exit_code, output = self.execute(
|
||||
f"sudo tar -xf {uploaded_path} -C {sandbox_dest} && sudo rm {uploaded_path}"
|
||||
)
|
||||
if exit_code != 0:
|
||||
raise Exception(
|
||||
f"Failed to extract {uploaded_path} to {sandbox_dest}: {output}"
|
||||
)
|
||||
|
||||
# Delete the local archive
|
||||
os.remove(tar_filename)
|
||||
|
||||
def close(self):
|
||||
# E2B v2 uses kill() instead of close()
|
||||
if hasattr(self.sandbox, 'kill'):
|
||||
self.sandbox.kill()
|
||||
elif hasattr(self.sandbox, 'close'):
|
||||
self.sandbox.close()
|
||||
|
||||
def get_working_directory(self):
|
||||
return self.sandbox.cwd
|
||||
|
||||
|
||||
# Alias for backward compatibility
|
||||
E2BSandbox = E2BBox
|
||||
-6
@@ -1,6 +0,0 @@
|
||||
"""Modal runtime implementation.
|
||||
|
||||
This runtime reads configuration directly from environment variables:
|
||||
- MODAL_TOKEN_ID: Modal API token ID for authentication
|
||||
- MODAL_TOKEN_SECRET: Modal API token secret for authentication
|
||||
"""
|
||||
-298
@@ -1,298 +0,0 @@
|
||||
import os
|
||||
import tempfile
|
||||
from time import sleep
|
||||
from pathlib import Path
|
||||
from typing import Callable
|
||||
|
||||
import httpx
|
||||
import modal
|
||||
import tenacity
|
||||
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
from openhands.events import EventStream
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient,
|
||||
)
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.command import get_action_execution_server_startup_command
|
||||
from openhands.runtime.utils.runtime_build import (
|
||||
BuildFromImageType,
|
||||
prep_build_folder,
|
||||
)
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
from openhands.utils.tenacity_stop import stop_if_should_exit
|
||||
|
||||
# FIXME: this will not work in HA mode. We need a better way to track IDs
|
||||
MODAL_RUNTIME_IDS: dict[str, str] = {}
|
||||
|
||||
|
||||
class ModalRuntime(ActionExecutionClient):
|
||||
"""This runtime will subscribe the event stream.
|
||||
|
||||
When receive an event, it will send the event to runtime-client which run inside the Modal sandbox environment.
|
||||
|
||||
Args:
|
||||
config (OpenHandsConfig): The application configuration.
|
||||
event_stream (EventStream): The event stream to subscribe to.
|
||||
sid (str, optional): The session ID. Defaults to 'default'.
|
||||
plugins (list[PluginRequirement] | None, optional): List of plugin requirements. Defaults to None.
|
||||
env_vars (dict[str, str] | None, optional): Environment variables to set. Defaults to None.
|
||||
"""
|
||||
|
||||
container_name_prefix = "openhands-sandbox-"
|
||||
sandbox: modal.Sandbox | None
|
||||
sid: str
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: OpenHandsConfig,
|
||||
event_stream: EventStream,
|
||||
sid: str = "default",
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
attach_to_existing: bool = False,
|
||||
headless_mode: bool = True,
|
||||
user_id: str | None = None,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
|
||||
):
|
||||
# Read Modal API credentials from environment variables
|
||||
modal_token_id = os.getenv("MODAL_TOKEN_ID")
|
||||
modal_token_secret = os.getenv("MODAL_TOKEN_SECRET")
|
||||
|
||||
if not modal_token_id:
|
||||
raise ValueError(
|
||||
"MODAL_TOKEN_ID environment variable is required for Modal runtime"
|
||||
)
|
||||
if not modal_token_secret:
|
||||
raise ValueError(
|
||||
"MODAL_TOKEN_SECRET environment variable is required for Modal runtime"
|
||||
)
|
||||
|
||||
self.config = config
|
||||
self.sandbox = None
|
||||
self.sid = sid
|
||||
|
||||
self.modal_client = modal.Client.from_credentials(
|
||||
modal_token_id,
|
||||
modal_token_secret,
|
||||
)
|
||||
self.app = modal.App.lookup(
|
||||
"openhands", create_if_missing=True, client=self.modal_client
|
||||
)
|
||||
|
||||
# workspace_base cannot be used because we can't bind mount into a sandbox.
|
||||
if self.config.workspace_base is not None:
|
||||
self.log(
|
||||
"warning",
|
||||
"Setting workspace_base is not supported in the modal runtime.",
|
||||
)
|
||||
|
||||
# This value is arbitrary as it's private to the container
|
||||
self.container_port = 3000
|
||||
self._vscode_port = 4445
|
||||
self._vscode_url: str | None = None
|
||||
|
||||
self.status_callback = status_callback
|
||||
self.base_container_image_id = self.config.sandbox.base_container_image
|
||||
self.runtime_container_image_id = self.config.sandbox.runtime_container_image
|
||||
|
||||
if self.config.sandbox.runtime_extra_deps:
|
||||
self.log(
|
||||
"debug",
|
||||
f"Installing extra user-provided dependencies in the runtime image: {self.config.sandbox.runtime_extra_deps}",
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
event_stream,
|
||||
sid,
|
||||
plugins,
|
||||
env_vars,
|
||||
status_callback,
|
||||
attach_to_existing,
|
||||
headless_mode,
|
||||
user_id,
|
||||
git_provider_tokens,
|
||||
)
|
||||
|
||||
async def connect(self):
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
self.log("debug", f"ModalRuntime `{self.sid}`")
|
||||
|
||||
self.image = self._get_image_definition(
|
||||
self.base_container_image_id,
|
||||
self.runtime_container_image_id,
|
||||
self.config.sandbox.runtime_extra_deps,
|
||||
)
|
||||
|
||||
if self.attach_to_existing:
|
||||
if self.sid in MODAL_RUNTIME_IDS:
|
||||
sandbox_id = MODAL_RUNTIME_IDS[self.sid]
|
||||
self.log("debug", f"Attaching to existing Modal sandbox: {sandbox_id}")
|
||||
self.sandbox = modal.Sandbox.from_id(
|
||||
sandbox_id, client=self.modal_client
|
||||
)
|
||||
else:
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
await call_sync_from_async(
|
||||
self._init_sandbox,
|
||||
sandbox_workspace_dir=self.config.workspace_mount_path_in_sandbox,
|
||||
plugins=self.plugins,
|
||||
)
|
||||
|
||||
self.set_runtime_status(RuntimeStatus.RUNTIME_STARTED)
|
||||
|
||||
if self.sandbox is None:
|
||||
raise Exception("Sandbox not initialized")
|
||||
tunnel = self.sandbox.tunnels()[self.container_port]
|
||||
self.api_url = tunnel.url
|
||||
self.log("info", "Waiting 20 secs for the container to be ready... (avoiding RemoteProtocolError)")
|
||||
sleep(20)
|
||||
self.log("debug", f"Container started. Server url: {self.api_url}")
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.log("debug", "Waiting for client to become ready...")
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
self._wait_until_alive()
|
||||
self.setup_initial_env()
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self._runtime_initialized = True
|
||||
|
||||
@property
|
||||
def action_execution_server_url(self):
|
||||
return self.api_url
|
||||
|
||||
@tenacity.retry(
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
retry=tenacity.retry_if_exception_type((ConnectionError, httpx.NetworkError)),
|
||||
reraise=True,
|
||||
wait=tenacity.wait_fixed(2),
|
||||
)
|
||||
def _wait_until_alive(self):
|
||||
self.check_if_alive()
|
||||
|
||||
def _get_image_definition(
|
||||
self,
|
||||
base_container_image_id: str | None,
|
||||
runtime_container_image_id: str | None,
|
||||
runtime_extra_deps: str | None,
|
||||
) -> modal.Image:
|
||||
if runtime_container_image_id:
|
||||
base_runtime_image = modal.Image.from_registry(runtime_container_image_id)
|
||||
elif base_container_image_id:
|
||||
build_folder = tempfile.mkdtemp()
|
||||
prep_build_folder(
|
||||
build_folder=Path(build_folder),
|
||||
base_image=base_container_image_id,
|
||||
build_from=BuildFromImageType.SCRATCH,
|
||||
extra_deps=runtime_extra_deps,
|
||||
enable_browser=True,
|
||||
)
|
||||
base_runtime_image = modal.Image.from_dockerfile(
|
||||
path=os.path.join(build_folder, "Dockerfile"),
|
||||
context_dir=build_folder,
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
"Neither runtime container image nor base container image is set"
|
||||
)
|
||||
|
||||
return base_runtime_image.run_commands(
|
||||
"""
|
||||
# Disable bracketed paste
|
||||
# https://github.com/pexpect/pexpect/issues/669
|
||||
echo "set enable-bracketed-paste off" >> /etc/inputrc && \\
|
||||
echo 'export INPUTRC=/etc/inputrc' >> /etc/bash.bashrc
|
||||
""".strip()
|
||||
)
|
||||
|
||||
@tenacity.retry(
|
||||
stop=tenacity.stop_after_attempt(5),
|
||||
wait=tenacity.wait_exponential(multiplier=1, min=4, max=60),
|
||||
)
|
||||
def _init_sandbox(
|
||||
self,
|
||||
sandbox_workspace_dir: str,
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
):
|
||||
try:
|
||||
self.log("debug", "Preparing to start container...")
|
||||
# Combine environment variables
|
||||
environment: dict[str, str | None] = {
|
||||
"port": str(self.container_port),
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"VSCODE_PORT": str(self._vscode_port),
|
||||
}
|
||||
if self.config.debug:
|
||||
environment["DEBUG"] = "true"
|
||||
|
||||
env_secret = modal.Secret.from_dict(environment)
|
||||
|
||||
self.log("debug", f"Sandbox workspace: {sandbox_workspace_dir}")
|
||||
sandbox_start_cmd = get_action_execution_server_startup_command(
|
||||
server_port=self.container_port,
|
||||
plugins=self.plugins,
|
||||
app_config=self.config,
|
||||
)
|
||||
self.log("debug", f"Starting container with command: {sandbox_start_cmd}")
|
||||
self.sandbox = modal.Sandbox.create(
|
||||
*sandbox_start_cmd,
|
||||
secrets=[env_secret],
|
||||
workdir="/openhands/code",
|
||||
encrypted_ports=[self.container_port, self._vscode_port],
|
||||
image=self.image,
|
||||
app=self.app,
|
||||
client=self.modal_client,
|
||||
timeout=60 * 60,
|
||||
)
|
||||
MODAL_RUNTIME_IDS[self.sid] = self.sandbox.object_id
|
||||
self.log("debug", f"Container started with modal sandbox ID: {self.sandbox.object_id}")
|
||||
|
||||
except Exception as e:
|
||||
self.log(
|
||||
"error", f"Error: Instance {self.sid} FAILED to start container!\n"
|
||||
)
|
||||
self.log("error", str(e))
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
def close(self):
|
||||
"""Closes the ModalRuntime and associated objects."""
|
||||
super().close()
|
||||
|
||||
if not self.attach_to_existing and self.sandbox:
|
||||
self.sandbox.terminate()
|
||||
|
||||
@property
|
||||
def vscode_url(self) -> str | None:
|
||||
if self._vscode_url is not None: # cached value
|
||||
self.log("debug", f"VSCode URL: {self._vscode_url}")
|
||||
return self._vscode_url
|
||||
token = super().get_vscode_token()
|
||||
if not token:
|
||||
self.log("error", "VSCode token not found")
|
||||
return None
|
||||
if not self.sandbox:
|
||||
self.log("error", "Sandbox not initialized")
|
||||
return None
|
||||
|
||||
tunnel = self.sandbox.tunnels()[self._vscode_port]
|
||||
tunnel_url = tunnel.url
|
||||
self._vscode_url = (
|
||||
tunnel_url
|
||||
+ f"/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}"
|
||||
)
|
||||
|
||||
self.log(
|
||||
"debug",
|
||||
f"VSCode URL: {self._vscode_url}",
|
||||
)
|
||||
|
||||
return self._vscode_url
|
||||
-31
@@ -1,31 +0,0 @@
|
||||
# Runloop Runtime
|
||||
Runloop provides a fast, secure and scalable AI sandbox (Devbox).
|
||||
Check out the [runloop docs](https://docs.runloop.ai/overview/what-is-runloop)
|
||||
for more detail
|
||||
|
||||
## Access
|
||||
Runloop is currently available in a closed beta. For early access, or
|
||||
just to say hello, sign up at https://www.runloop.ai/hello
|
||||
|
||||
## Set up
|
||||
With your runloop API,
|
||||
```bash
|
||||
export RUNLOOP_API_KEY=<your-api-key>
|
||||
```
|
||||
|
||||
Configure the runtime
|
||||
```bash
|
||||
export RUNTIME="runloop"
|
||||
```
|
||||
|
||||
## Interact with your devbox
|
||||
Runloop provides additional tools to interact with your Devbox based
|
||||
runtime environment. See the [docs](https://docs.runloop.ai/tools) for an up
|
||||
to date list of tools.
|
||||
|
||||
### Dashboard
|
||||
View logs, ssh into, or view your Devbox status from the [dashboard](https://platform.runloop.ai)
|
||||
|
||||
### CLI
|
||||
Use the Runloop CLI to view logs, execute commands, and more.
|
||||
See the setup instructions [here](https://docs.runloop.ai/tools/cli)
|
||||
@@ -1,5 +0,0 @@
|
||||
"""Runloop runtime implementation.
|
||||
|
||||
This runtime reads configuration directly from environment variables:
|
||||
- RUNLOOP_API_KEY: API key for Runloop authentication
|
||||
"""
|
||||
-205
@@ -1,205 +0,0 @@
|
||||
import logging
|
||||
import os
|
||||
from typing import Callable
|
||||
|
||||
import tenacity
|
||||
from runloop_api_client import Runloop
|
||||
from runloop_api_client.types import DevboxView
|
||||
from runloop_api_client.types.shared_params import LaunchParameters
|
||||
|
||||
from openhands.core.config import OpenHandsConfig
|
||||
from openhands.core.logger import openhands_logger as logger
|
||||
from openhands.events import EventStream
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient,
|
||||
)
|
||||
from openhands.runtime.plugins import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.command import get_action_execution_server_startup_command
|
||||
from openhands.utils.tenacity_stop import stop_if_should_exit
|
||||
|
||||
CONTAINER_NAME_PREFIX = "openhands-runtime-"
|
||||
|
||||
|
||||
class RunloopRuntime(ActionExecutionClient):
|
||||
"""The RunloopRuntime class is an DockerRuntime that utilizes Runloop Devbox as a runtime environment."""
|
||||
|
||||
_sandbox_port: int = 4444
|
||||
_vscode_port: int = 4445
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: OpenHandsConfig,
|
||||
event_stream: EventStream,
|
||||
sid: str = "default",
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
attach_to_existing: bool = False,
|
||||
headless_mode: bool = True,
|
||||
user_id: str | None = None,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
|
||||
):
|
||||
# Read Runloop API key from environment variable
|
||||
runloop_api_key = os.getenv("RUNLOOP_API_KEY")
|
||||
if not runloop_api_key:
|
||||
raise ValueError(
|
||||
"RUNLOOP_API_KEY environment variable is required for Runloop runtime"
|
||||
)
|
||||
|
||||
self.devbox: DevboxView | None = None
|
||||
self.config = config
|
||||
self.runloop_api_client = Runloop(
|
||||
bearer_token=runloop_api_key,
|
||||
)
|
||||
self.container_name = CONTAINER_NAME_PREFIX + sid
|
||||
super().__init__(
|
||||
config,
|
||||
event_stream,
|
||||
sid,
|
||||
plugins,
|
||||
env_vars,
|
||||
status_callback,
|
||||
attach_to_existing,
|
||||
headless_mode,
|
||||
user_id,
|
||||
git_provider_tokens,
|
||||
)
|
||||
# Buffer for container logs
|
||||
self._vscode_url: str | None = None
|
||||
|
||||
@property
|
||||
def action_execution_server_url(self):
|
||||
return self.api_url
|
||||
|
||||
@tenacity.retry(
|
||||
stop=tenacity.stop_after_attempt(120),
|
||||
wait=tenacity.wait_fixed(1),
|
||||
)
|
||||
def _wait_for_devbox(self, devbox: DevboxView) -> DevboxView:
|
||||
"""Pull devbox status until it is running"""
|
||||
if devbox == "running":
|
||||
return devbox
|
||||
|
||||
devbox = self.runloop_api_client.devboxes.retrieve(id=devbox.id)
|
||||
if devbox.status != "running":
|
||||
raise ConnectionRefusedError("Devbox is not running")
|
||||
|
||||
# Devbox is connected and running
|
||||
logging.debug(f"devbox.id={devbox.id} is running")
|
||||
return devbox
|
||||
|
||||
def _create_new_devbox(self) -> DevboxView:
|
||||
# Note: Runloop connect
|
||||
start_command = get_action_execution_server_startup_command(
|
||||
server_port=self._sandbox_port,
|
||||
plugins=self.plugins,
|
||||
app_config=self.config,
|
||||
)
|
||||
|
||||
# Add some additional commands based on our image
|
||||
# NB: start off as root, action_execution_server will ultimately choose user but expects all context
|
||||
# (ie browser) to be installed as root
|
||||
# Convert start_command list to a single command string with additional setup
|
||||
start_command_str = (
|
||||
"export MAMBA_ROOT_PREFIX=/openhands/micromamba && "
|
||||
"cd /openhands/code && "
|
||||
"/openhands/micromamba/bin/micromamba run -n openhands poetry config virtualenvs.path /openhands/poetry && "
|
||||
+ " ".join(start_command)
|
||||
)
|
||||
entrypoint = f"sudo bash -c '{start_command_str}'"
|
||||
|
||||
devbox = self.runloop_api_client.devboxes.create(
|
||||
entrypoint=entrypoint,
|
||||
name=self.sid,
|
||||
environment_variables={"DEBUG": "true"} if self.config.debug else {},
|
||||
prebuilt="openhands",
|
||||
launch_parameters=LaunchParameters(
|
||||
available_ports=[self._sandbox_port, self._vscode_port],
|
||||
resource_size_request="LARGE",
|
||||
launch_commands=[
|
||||
f"mkdir -p {self.config.workspace_mount_path_in_sandbox}"
|
||||
],
|
||||
),
|
||||
metadata={"container-name": self.container_name},
|
||||
)
|
||||
return self._wait_for_devbox(devbox)
|
||||
|
||||
async def connect(self):
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
if self.attach_to_existing:
|
||||
active_devboxes = self.runloop_api_client.devboxes.list(
|
||||
status="running"
|
||||
).devboxes
|
||||
self.devbox = next(
|
||||
(devbox for devbox in active_devboxes if devbox.name == self.sid), None
|
||||
)
|
||||
|
||||
if self.devbox is None:
|
||||
self.devbox = self._create_new_devbox()
|
||||
|
||||
# Create tunnel - this will return a stable url, so is safe to call if we are attaching to existing
|
||||
tunnel = self.runloop_api_client.devboxes.create_tunnel(
|
||||
id=self.devbox.id,
|
||||
port=self._sandbox_port,
|
||||
)
|
||||
|
||||
self.api_url = tunnel.url
|
||||
logger.info(f"Container started. Server url: {self.api_url}")
|
||||
|
||||
# End Runloop connect
|
||||
# NOTE: Copied from DockerRuntime
|
||||
logger.info("Waiting for client to become ready...")
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self._wait_until_alive()
|
||||
|
||||
if not self.attach_to_existing:
|
||||
self.setup_initial_env()
|
||||
|
||||
logger.info(
|
||||
f"Container initialized with plugins: {[plugin.name for plugin in self.plugins]}"
|
||||
)
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
|
||||
@tenacity.retry(
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
wait=tenacity.wait_fixed(1),
|
||||
reraise=(ConnectionRefusedError,),
|
||||
)
|
||||
def _wait_until_alive(self):
|
||||
super().check_if_alive()
|
||||
|
||||
def close(self, rm_all_containers: bool | None = True):
|
||||
super().close()
|
||||
|
||||
if self.attach_to_existing:
|
||||
return
|
||||
|
||||
if self.devbox:
|
||||
self.runloop_api_client.devboxes.shutdown(self.devbox.id)
|
||||
|
||||
@property
|
||||
def vscode_url(self) -> str | None:
|
||||
if self._vscode_url is not None: # cached value
|
||||
return self._vscode_url
|
||||
token = super().get_vscode_token()
|
||||
if not token:
|
||||
return None
|
||||
if not self.devbox:
|
||||
return None
|
||||
self._vscode_url = (
|
||||
self.runloop_api_client.devboxes.create_tunnel(
|
||||
id=self.devbox.id,
|
||||
port=self._vscode_port,
|
||||
).url
|
||||
+ f"/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}"
|
||||
)
|
||||
|
||||
self.log(
|
||||
"debug",
|
||||
f"VSCode URL: {self._vscode_url}",
|
||||
)
|
||||
|
||||
return self._vscode_url
|
||||
Reference in New Issue
Block a user