diff --git a/.agents/skills/update-sdk/references/docker-image-locations.md b/.agents/skills/update-sdk/references/docker-image-locations.md index 3fb466ba01..1971dc9a87 100644 --- a/.agents/skills/update-sdk/references/docker-image-locations.md +++ b/.agents/skills/update-sdk/references/docker-image-locations.md @@ -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 | diff --git a/containers/app/entrypoint.sh b/containers/app/entrypoint.sh index ef1253e7b1..28e937d890 100644 --- a/containers/app/entrypoint.sh +++ b/containers/app/entrypoint.sh @@ -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 diff --git a/dev_config/python/mypy.ini b/dev_config/python/mypy.ini index b2e656e508..0e371c2a7a 100644 --- a/dev_config/python/mypy.ini +++ b/dev_config/python/mypy.ini @@ -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 diff --git a/enterprise/poetry.lock b/enterprise/poetry.lock index 7e5023097c..8df31e95ae 100644 --- a/enterprise/poetry.lock +++ b/enterprise/poetry.lock @@ -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 = ".." diff --git a/openhands/runtime/__init__.py b/openhands/runtime/__init__.py index f987ad3f2d..800323b71a 100644 --- a/openhands/runtime/__init__.py +++ b/openhands/runtime/__init__.py @@ -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__) diff --git a/poetry.lock b/poetry.lock index 8c096a6124..ee6798719a 100644 --- a/poetry.lock +++ b/poetry.lock @@ -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" diff --git a/pyproject.toml b/pyproject.toml index c8448ed46e..3159b19dbe 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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 diff --git a/tests/unit/runtime/test_runtime_import_robustness.py b/tests/unit/runtime/test_runtime_import_robustness.py index 6e9f87b597..b3304a00e2 100644 --- a/tests/unit/runtime/test_runtime_import_robustness.py +++ b/tests/unit/runtime/test_runtime_import_robustness.py @@ -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 diff --git a/third_party/__init__.py b/third_party/__init__.py deleted file mode 100644 index e55d3a2758..0000000000 --- a/third_party/__init__.py +++ /dev/null @@ -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 -""" diff --git a/third_party/containers/e2b-sandbox/Dockerfile b/third_party/containers/e2b-sandbox/Dockerfile deleted file mode 100644 index 12b4a336b0..0000000000 --- a/third_party/containers/e2b-sandbox/Dockerfile +++ /dev/null @@ -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/* diff --git a/third_party/containers/e2b-sandbox/README.md b/third_party/containers/e2b-sandbox/README.md deleted file mode 100644 index 9f9ff72888..0000000000 --- a/third_party/containers/e2b-sandbox/README.md +++ /dev/null @@ -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" - ``` diff --git a/third_party/containers/e2b-sandbox/e2b.toml b/third_party/containers/e2b-sandbox/e2b.toml deleted file mode 100644 index f19eb933a0..0000000000 --- a/third_party/containers/e2b-sandbox/e2b.toml +++ /dev/null @@ -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" diff --git a/third_party/runtime/__init__.py b/third_party/runtime/__init__.py deleted file mode 100644 index 342b8eb41b..0000000000 --- a/third_party/runtime/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Third-party runtime implementations.""" diff --git a/third_party/runtime/impl/__init__.py b/third_party/runtime/impl/__init__.py deleted file mode 100644 index e23e398b65..0000000000 --- a/third_party/runtime/impl/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Third-party runtime implementation modules.""" diff --git a/third_party/runtime/impl/daytona/README.md b/third_party/runtime/impl/daytona/README.md deleted file mode 100644 index 2222954499..0000000000 --- a/third_party/runtime/impl/daytona/README.md +++ /dev/null @@ -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 `` with the actual key you copied: - -Mac/Linux: -```bash -export DAYTONA_API_KEY="" -``` - -Windows PowerShell: -```powershell -$env:DAYTONA_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 `` 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="" # e.g. 0.27 -``` - -#### Windows PowerShell: -```powershell -$env:OPENHANDS_VERSION="" # 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 `` with the actual key you copied: - -#### Mac/Linux: -```bash -export DAYTONA_API_KEY="" -``` - -#### Windows PowerShell: -```powershell -$env:DAYTONA_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="" -``` - -#### Windows PowerShell: -```powershell -$env:RUNTIME="daytona" -$env:DAYTONA_API_KEY="" -``` - -## Documentation -Read more by visiting our [documentation](https://www.daytona.io/docs/) page. diff --git a/third_party/runtime/impl/daytona/__init__.py b/third_party/runtime/impl/daytona/__init__.py deleted file mode 100644 index ae61d8f7ef..0000000000 --- a/third_party/runtime/impl/daytona/__init__.py +++ /dev/null @@ -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') -""" diff --git a/third_party/runtime/impl/daytona/daytona_runtime.py b/third_party/runtime/impl/daytona/daytona_runtime.py deleted file mode 100644 index d922cb4c77..0000000000 --- a/third_party/runtime/impl/daytona/daytona_runtime.py +++ /dev/null @@ -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)}." diff --git a/third_party/runtime/impl/e2b/README.md b/third_party/runtime/impl/e2b/README.md deleted file mode 100644 index b759132377..0000000000 --- a/third_party/runtime/impl/e2b/README.md +++ /dev/null @@ -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 - ``` - -## Links -- [E2B Docs](https://e2b.dev/docs) -- [E2B GitHub](https://github.com/e2b-dev/e2b) diff --git a/third_party/runtime/impl/e2b/__init__.py b/third_party/runtime/impl/e2b/__init__.py deleted file mode 100644 index 89ec333926..0000000000 --- a/third_party/runtime/impl/e2b/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""E2B runtime implementation. - -This runtime reads configuration directly from environment variables: -- E2B_API_KEY: API key for E2B authentication -""" diff --git a/third_party/runtime/impl/e2b/e2b_runtime.py b/third_party/runtime/impl/e2b/e2b_runtime.py deleted file mode 100644 index 92ec319edf..0000000000 --- a/third_party/runtime/impl/e2b/e2b_runtime.py +++ /dev/null @@ -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 diff --git a/third_party/runtime/impl/e2b/filestore.py b/third_party/runtime/impl/e2b/filestore.py deleted file mode 100644 index dc7f669850..0000000000 --- a/third_party/runtime/impl/e2b/filestore.py +++ /dev/null @@ -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) diff --git a/third_party/runtime/impl/e2b/sandbox.py b/third_party/runtime/impl/e2b/sandbox.py deleted file mode 100644 index ff5cbdbe07..0000000000 --- a/third_party/runtime/impl/e2b/sandbox.py +++ /dev/null @@ -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 diff --git a/third_party/runtime/impl/modal/__init__.py b/third_party/runtime/impl/modal/__init__.py deleted file mode 100644 index 45906cd24b..0000000000 --- a/third_party/runtime/impl/modal/__init__.py +++ /dev/null @@ -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 -""" diff --git a/third_party/runtime/impl/modal/modal_runtime.py b/third_party/runtime/impl/modal/modal_runtime.py deleted file mode 100644 index 12aab14271..0000000000 --- a/third_party/runtime/impl/modal/modal_runtime.py +++ /dev/null @@ -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 diff --git a/third_party/runtime/impl/runloop/README.md b/third_party/runtime/impl/runloop/README.md deleted file mode 100644 index 513a0077e3..0000000000 --- a/third_party/runtime/impl/runloop/README.md +++ /dev/null @@ -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= -``` - -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) diff --git a/third_party/runtime/impl/runloop/__init__.py b/third_party/runtime/impl/runloop/__init__.py deleted file mode 100644 index 2f4becbf24..0000000000 --- a/third_party/runtime/impl/runloop/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -"""Runloop runtime implementation. - -This runtime reads configuration directly from environment variables: -- RUNLOOP_API_KEY: API key for Runloop authentication -""" diff --git a/third_party/runtime/impl/runloop/runloop_runtime.py b/third_party/runtime/impl/runloop/runloop_runtime.py deleted file mode 100644 index 4fb70c3904..0000000000 --- a/third_party/runtime/impl/runloop/runloop_runtime.py +++ /dev/null @@ -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