Removed V0 third party runtimes (#14119)

This commit is contained in:
Tim O'Farrell
2026-04-24 12:23:01 -06:00
committed by GitHub
parent 180a35f013
commit 77043da280
27 changed files with 13 additions and 2301 deletions
@@ -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 |
-12
View File
@@ -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
+1 -1
View File
@@ -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
+1 -4
View File
@@ -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 = ".."
+1 -73
View File
@@ -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
View File
@@ -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"
-10
View File
@@ -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
-14
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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"
-1
View File
@@ -1 +0,0 @@
"""Third-party runtime implementations."""
-1
View File
@@ -1 +0,0 @@
"""Third-party runtime implementation modules."""
-134
View File
@@ -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.
-7
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -1,5 +0,0 @@
"""E2B runtime implementation.
This runtime reads configuration directly from environment variables:
- E2B_API_KEY: API key for E2B authentication
"""
-402
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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)
-5
View File
@@ -1,5 +0,0 @@
"""Runloop runtime implementation.
This runtime reads configuration directly from environment variables:
- RUNLOOP_API_KEY: API key for Runloop authentication
"""
-205
View File
@@ -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