diff --git a/config.template.toml b/config.template.toml index 0c83cc6ccd..4071f94cad 100644 --- a/config.template.toml +++ b/config.template.toml @@ -10,18 +10,7 @@ # General core configurations ############################################################################## [core] -# API key for E2B -#e2b_api_key = "" - -# API key for Modal -#modal_api_token_id = "" -#modal_api_token_secret = "" - -# API key for Daytona -#daytona_api_key = "" - -# Daytona Target -#daytona_target = "" +# API keys and configuration for core services # Base path for the workspace #workspace_base = "./workspace" diff --git a/dev_config/python/.pre-commit-config.yaml b/dev_config/python/.pre-commit-config.yaml index 56053cba2f..6239e2213f 100644 --- a/dev_config/python/.pre-commit-config.yaml +++ b/dev_config/python/.pre-commit-config.yaml @@ -3,9 +3,9 @@ repos: rev: v5.0.0 hooks: - id: trailing-whitespace - exclude: ^(docs/|modules/|python/|openhands-ui/) + exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/) - id: end-of-file-fixer - exclude: ^(docs/|modules/|python/|openhands-ui/) + exclude: ^(docs/|modules/|python/|openhands-ui/|third_party/) - id: check-yaml args: ["--allow-multiple-documents"] - id: debug-statements @@ -28,10 +28,12 @@ repos: entry: ruff check --config dev_config/python/ruff.toml types_or: [python, pyi, jupyter] args: [--fix, --unsafe-fixes] + exclude: third_party/ # Run the formatter. - id: ruff-format entry: ruff format --config dev_config/python/ruff.toml types_or: [python, pyi, jupyter] + exclude: third_party/ - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.15.0 diff --git a/dev_config/python/mypy.ini b/dev_config/python/mypy.ini index 84b97d720b..f8ca5c8c59 100644 --- a/dev_config/python/mypy.ini +++ b/dev_config/python/mypy.ini @@ -7,3 +7,5 @@ warn_unreachable = True warn_redundant_casts = True no_implicit_optional = True strict_optional = True +# Exclude third-party runtime directory from type checking +exclude = third_party/ diff --git a/dev_config/python/ruff.toml b/dev_config/python/ruff.toml index e15500e061..4df1c4d97b 100644 --- a/dev_config/python/ruff.toml +++ b/dev_config/python/ruff.toml @@ -1,3 +1,6 @@ +# Exclude third-party runtime directory from linting +exclude = ["third_party/"] + [lint] select = [ "E", diff --git a/docs/usage/configuration-options.mdx b/docs/usage/configuration-options.mdx index 8d9260ad61..439b8b28bf 100644 --- a/docs/usage/configuration-options.mdx +++ b/docs/usage/configuration-options.mdx @@ -12,22 +12,6 @@ description: This page outlines all available configuration options for OpenHand The core configuration options are defined in the `[core]` section of the `config.toml` file. -### API Keys -- `e2b_api_key` - - Type: `str` - - Default: `""` - - Description: API key for E2B - -- `modal_api_token_id` - - Type: `str` - - Default: `""` - - Description: API token ID for Modal - -- `modal_api_token_secret` - - Type: `str` - - Default: `""` - - Description: API token secret for Modal - ### Workspace - `workspace_base` **(Deprecated)** - Type: `str` diff --git a/docs/usage/runtimes/overview.mdx b/docs/usage/runtimes/overview.mdx index 4513aa5ea0..25ab882cc7 100644 --- a/docs/usage/runtimes/overview.mdx +++ b/docs/usage/runtimes/overview.mdx @@ -9,8 +9,6 @@ commands. By default, OpenHands uses a [Docker-based runtime](/usage/runtimes/docker), running on your local computer. This means you only have to pay for the LLM you're using, and your code is only ever sent to the LLM. -We also support other runtimes, which are typically managed by third-parties. - Additionally, we provide a [Local Runtime](/usage/runtimes/local) that runs directly on your machine without Docker, which can be useful in controlled environments like CI pipelines. @@ -21,6 +19,18 @@ OpenHands supports several different runtime environments: - [Docker Runtime](/usage/runtimes/docker) - The default runtime that uses Docker containers for isolation (recommended for most users). - [OpenHands Remote Runtime](/usage/runtimes/remote) - Cloud-based runtime for parallel execution (beta). - [Local Runtime](/usage/runtimes/local) - Direct execution on your local machine without Docker. -- And more third-party runtimes: - - [Modal Runtime](/usage/runtimes/modal) - Runtime provided by our partners at Modal. - - [Daytona Runtime](/usage/runtimes/daytona) - Runtime provided by Daytona. + +### Third-Party Runtimes + +The following third-party runtimes are available when you install the `third_party_runtimes` extra: + +```bash +pip install openhands-ai[third_party_runtimes] +``` + +- [E2B Runtime](/usage/runtimes/e2b) - Open source runtime using E2B sandboxes. +- [Modal Runtime](/usage/runtimes/modal) - Serverless runtime using Modal infrastructure. +- [Runloop Runtime](/usage/runtimes/runloop) - Cloud runtime using Runloop infrastructure. +- [Daytona Runtime](/usage/runtimes/daytona) - Development environment runtime using Daytona. + +**Note**: These third-party runtimes are supported by their respective developers, not by the OpenHands team. For issues specific to these runtimes, please refer to their documentation or contact their support teams. diff --git a/openhands/core/config/openhands_config.py b/openhands/core/config/openhands_config.py index d2908f0530..10a9b2872e 100644 --- a/openhands/core/config/openhands_config.py +++ b/openhands/core/config/openhands_config.py @@ -46,7 +46,6 @@ class OpenHandsConfig(BaseModel): run_as_openhands: Whether to run as openhands. max_iterations: Maximum number of iterations allowed. max_budget_per_task: Maximum budget per task, agent stops if exceeded. - e2b_api_key: E2B API key. disable_color: Whether to disable terminal colors. For terminals that don't support color. debug: Whether to enable debugging mode. file_uploads_max_file_size_mb: Maximum file upload size in MB. `0` means unlimited. @@ -88,19 +87,14 @@ class OpenHandsConfig(BaseModel): run_as_openhands: bool = Field(default=True) max_iterations: int = Field(default=OH_MAX_ITERATIONS) max_budget_per_task: float | None = Field(default=None) - e2b_api_key: SecretStr | None = Field(default=None) - modal_api_token_id: SecretStr | None = Field(default=None) - modal_api_token_secret: SecretStr | None = Field(default=None) + disable_color: bool = Field(default=False) jwt_secret: SecretStr | None = Field(default=None) debug: bool = Field(default=False) file_uploads_max_file_size_mb: int = Field(default=0) file_uploads_restrict_file_types: bool = Field(default=False) file_uploads_allowed_extensions: list[str] = Field(default_factory=lambda: ['.*']) - runloop_api_key: SecretStr | None = Field(default=None) - daytona_api_key: SecretStr | None = Field(default=None) - daytona_api_url: str = Field(default='https://app.daytona.io/api') - daytona_target: str = Field(default='eu') + cli_multiline_input: bool = Field(default=False) conversation_max_age_seconds: int = Field(default=864000) # 10 days in seconds enable_default_condenser: bool = Field(default=True) diff --git a/openhands/core/logger.py b/openhands/core/logger.py index 9e1328cb8f..b10f8aee89 100644 --- a/openhands/core/logger.py +++ b/openhands/core/logger.py @@ -261,6 +261,7 @@ class SensitiveDataFilter(logging.Filter): 'modal_api_token_secret', 'llm_api_key', 'sandbox_env_github_token', + 'runloop_api_key', 'daytona_api_key', ] diff --git a/openhands/runtime/__init__.py b/openhands/runtime/__init__.py index 45f539ef2c..2e188f8844 100644 --- a/openhands/runtime/__init__.py +++ b/openhands/runtime/__init__.py @@ -1,31 +1,84 @@ +import importlib + from openhands.runtime.base import Runtime from openhands.runtime.impl.cli.cli_runtime import CLIRuntime -from openhands.runtime.impl.daytona.daytona_runtime import DaytonaRuntime from openhands.runtime.impl.docker.docker_runtime import ( DockerRuntime, ) -from openhands.runtime.impl.e2b.e2b_runtime import E2BRuntime from openhands.runtime.impl.kubernetes.kubernetes_runtime import KubernetesRuntime from openhands.runtime.impl.local.local_runtime import LocalRuntime -from openhands.runtime.impl.modal.modal_runtime import ModalRuntime from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime -from openhands.runtime.impl.runloop.runloop_runtime import RunloopRuntime from openhands.utils.import_utils import get_impl # mypy: disable-error-code="type-abstract" _DEFAULT_RUNTIME_CLASSES: dict[str, type[Runtime]] = { 'eventstream': DockerRuntime, 'docker': DockerRuntime, - 'e2b': E2BRuntime, 'remote': RemoteRuntime, - 'modal': ModalRuntime, - 'runloop': RunloopRuntime, 'local': LocalRuntime, - 'daytona': DaytonaRuntime, 'kubernetes': KubernetesRuntime, '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: + 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]: """ @@ -33,26 +86,28 @@ def get_runtime_cls(name: str) -> type[Runtime]: Otherwise attempt to resolve name as subclass of Runtime and return it. Raise on invalid selections. """ - if name in _DEFAULT_RUNTIME_CLASSES: - return _DEFAULT_RUNTIME_CLASSES[name] + if name in _ALL_RUNTIME_CLASSES: + return _ALL_RUNTIME_CLASSES[name] try: return get_impl(Runtime, name) except Exception as e: - known_keys = _DEFAULT_RUNTIME_CLASSES.keys() + known_keys = _ALL_RUNTIME_CLASSES.keys() raise ValueError( f'Runtime {name} not supported, known are: {known_keys}' ) from e +# Build __all__ list dynamically based on available runtimes __all__ = [ 'Runtime', - 'E2BRuntime', 'RemoteRuntime', - 'ModalRuntime', - 'RunloopRuntime', 'DockerRuntime', - 'DaytonaRuntime', 'KubernetesRuntime', 'CLIRuntime', + '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/openhands/runtime/base.py b/openhands/runtime/base.py index 53b6aaff90..83021fbf3f 100644 --- a/openhands/runtime/base.py +++ b/openhands/runtime/base.py @@ -100,11 +100,10 @@ class Runtime(FileEditRuntimeMixin): Built-in implementations include: - DockerRuntime: Containerized environment using Docker - - E2BRuntime: Secure sandbox using E2B - RemoteRuntime: Remote execution environment - - ModalRuntime: Scalable cloud environment using Modal - LocalRuntime: Local execution for development - - DaytonaRuntime: Cloud development environment using Daytona + - KubernetesRuntime: Kubernetes-based execution environment + - CLIRuntime: Command-line interface runtime Args: sid: Session ID that uniquely identifies the current user session diff --git a/openhands/runtime/impl/__init__.py b/openhands/runtime/impl/__init__.py index 15e877811c..a4e9701ed4 100644 --- a/openhands/runtime/impl/__init__.py +++ b/openhands/runtime/impl/__init__.py @@ -6,22 +6,14 @@ from openhands.runtime.impl.action_execution.action_execution_client import ( ActionExecutionClient, ) from openhands.runtime.impl.cli import CLIRuntime -from openhands.runtime.impl.daytona.daytona_runtime import DaytonaRuntime from openhands.runtime.impl.docker.docker_runtime import DockerRuntime -from openhands.runtime.impl.e2b.e2b_runtime import E2BRuntime from openhands.runtime.impl.local.local_runtime import LocalRuntime -from openhands.runtime.impl.modal.modal_runtime import ModalRuntime from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime -from openhands.runtime.impl.runloop.runloop_runtime import RunloopRuntime __all__ = [ 'ActionExecutionClient', 'CLIRuntime', - 'DaytonaRuntime', 'DockerRuntime', - 'E2BRuntime', 'LocalRuntime', - 'ModalRuntime', 'RemoteRuntime', - 'RunloopRuntime', ] diff --git a/poetry.lock b/poetry.lock index eef3fc65fa..33615a8fef 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,12 +1,13 @@ -# This file is automatically @generated by Poetry 2.1.1 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.1.3 and should not be changed by hand. [[package]] name = "aioboto3" version = "14.3.0" description = "Async boto3 wrapper" -optional = false +optional = true python-versions = "<4.0,>=3.8" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "aioboto3-14.3.0-py3-none-any.whl", hash = "sha256:aec5de94e9edc1ffbdd58eead38a37f00ddac59a519db749a910c20b7b81bca7"}, {file = "aioboto3-14.3.0.tar.gz", hash = "sha256:1d18f88bb56835c607b62bb6cb907754d717bedde3ddfff6935727cb48a80135"}, @@ -24,9 +25,10 @@ s3cse = ["cryptography (>=44.0.1)"] name = "aiobotocore" version = "2.22.0" description = "Async client for aws services using botocore and aiohttp" -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "aiobotocore-2.22.0-py3-none-any.whl", hash = "sha256:b4e6306f79df9d81daff1f9d63189a2dbee4b77ce3ab937304834e35eaaeeccf"}, {file = "aiobotocore-2.22.0.tar.gz", hash = "sha256:11091477266b75c2b5d28421c1f2bc9a87d175d0b8619cb830805e7a113a170b"}, @@ -50,9 +52,10 @@ boto3 = ["boto3 (>=1.37.2,<1.37.4)"] name = "aiofiles" version = "24.1.0" description = "File support for asyncio." -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5"}, {file = "aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c"}, @@ -182,9 +185,10 @@ speedups = ["Brotli ; platform_python_implementation == \"CPython\"", "aiodns (> name = "aiohttp-retry" version = "2.9.1" description = "Simple retry client for aiohttp" -optional = false +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"}, @@ -197,9 +201,10 @@ aiohttp = "*" name = "aioitertools" version = "0.12.0" description = "itertools and builtins for AsyncIO and mixed iterables" -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "aioitertools-0.12.0-py3-none-any.whl", hash = "sha256:fc1f5fac3d737354de8831cbba3eb04f79dd649d8f3afb4c5b114925e662a796"}, {file = "aioitertools-0.12.0.tar.gz", hash = "sha256:c2a9055b4fbb7705f561b9d86053e8af5d10cc845d22c32008c43490b2d8dd6b"}, @@ -462,7 +467,7 @@ description = "LTS Port of Python audioop" optional = false python-versions = ">=3.13" groups = ["main"] -markers = "python_version >= \"3.13\"" +markers = "python_version == \"3.13\"" files = [ {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd1345ae99e17e6910f47ce7d52673c6a1a70820d78b67de1b7abb3af29c426a"}, {file = "audioop_lts-0.2.1-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:e175350da05d2087e12cea8e72a70a1a8b14a17e92ed2022952a4419689ede5e"}, @@ -1644,7 +1649,7 @@ files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, ] -markers = {main = "platform_system == \"Windows\" or sys_platform == \"win32\" or os_name == \"nt\"", dev = "os_name == \"nt\" or sys_platform == \"win32\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""} +markers = {main = "platform_system == \"Windows\" or os_name == \"nt\" or sys_platform == \"win32\"", dev = "os_name == \"nt\" or sys_platform == \"win32\"", runtime = "sys_platform == \"win32\"", test = "platform_system == \"Windows\" or sys_platform == \"win32\""} [[package]] name = "comm" @@ -2000,9 +2005,10 @@ vision = ["Pillow (>=9.4.0)"] name = "daytona" version = "0.21.1" description = "Python SDK for Daytona" -optional = false +optional = true python-versions = ">=3.7" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "daytona-0.21.1-py3-none-any.whl", hash = "sha256:1ce6b352f52ef92e667098b7bdaa60c22ffbfb8e686a8cbd12418bf7698ac834"}, {file = "daytona-0.21.1.tar.gz", hash = "sha256:01d83dd2b627f87e82491fb97f41845768d75c33f0767eaa44f6e8378bd58e60"}, @@ -2032,9 +2038,10 @@ dev = ["black[jupyter] (>=23.1.0,<24.0.0)", "build (>=1.0.3)", "isort (>=5.10.0, name = "daytona-api-client" version = "0.21.0" description = "Daytona" -optional = false +optional = true python-versions = "*" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "daytona_api_client-0.21.0-py3-none-any.whl", hash = "sha256:a8ff1f0fb397368dbd6ddb224c28d679e599c657eab2ec5821cf0c972a60229a"}, {file = "daytona_api_client-0.21.0.tar.gz", hash = "sha256:92d591c5a1750a827b5850425ce483441609b72b05d35a618d5353fbbba50bca"}, @@ -2050,9 +2057,10 @@ urllib3 = ">=1.25.3,<3.0.0" name = "daytona-api-client-async" version = "0.21.0" description = "Daytona" -optional = false +optional = true python-versions = "*" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "daytona_api_client_async-0.21.0-py3-none-any.whl", hash = "sha256:f5731963d0dd6c1e207b92bdc7f5b59952d3365444bc9dc8b013d77a4dddf377"}, {file = "daytona_api_client_async-0.21.0.tar.gz", hash = "sha256:08a22c0d1616f82efa8d157d7be6c432554fd43d75560725c4e0cef0228607d6"}, @@ -2317,9 +2325,10 @@ files = [ name = "e2b" version = "1.5.2" description = "E2B SDK that give agents cloud environments" -optional = false +optional = true python-versions = "<4.0,>=3.9" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "e2b-1.5.2-py3-none-any.whl", hash = "sha256:8cf755f2ff04098daa7ac778f768eee1df730a6181637fe124210345999890b3"}, {file = "e2b-1.5.2.tar.gz", hash = "sha256:29ed891ae04ffafff1744c57eff55901200f15030d34ac3fe76d6672e2bf7845"}, @@ -2349,9 +2358,10 @@ files = [ name = "environs" version = "9.5.0" description = "simplified environment variable parsing" -optional = false +optional = true python-versions = ">=3.6" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "environs-9.5.0-py2.py3-none-any.whl", hash = "sha256:1e549569a3de49c05f856f40bce86979e7d5ffbbc4398e7f338574c220189124"}, {file = "environs-9.5.0.tar.gz", hash = "sha256:a76307b36fbe856bdca7ee9161e6c466fd7fcffc297109a118c59b54e27e30c9"}, @@ -3053,8 +3063,8 @@ files = [ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0dev", extras = ["grpc"]} google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0dev" proto-plus = [ - {version = ">=1.22.3,<2.0.0dev"}, {version = ">=1.25.0,<2.0.0dev", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0dev"}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<6.0.0dev" @@ -3076,8 +3086,8 @@ googleapis-common-protos = ">=1.56.2,<2.0.0" grpcio = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} grpcio-status = {version = ">=1.49.1,<2.0.0", optional = true, markers = "python_version >= \"3.11\" and extra == \"grpc\""} proto-plus = [ - {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.19.5,<3.20.0 || >3.20.0,<3.20.1 || >3.20.1,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" requests = ">=2.18.0,<3.0.0" @@ -3295,8 +3305,8 @@ google-api-core = {version = ">=1.34.1,<2.0.dev0 || >=2.11.dev0,<3.0.0", extras google-auth = ">=2.14.1,<2.24.0 || >2.24.0,<2.25.0 || >2.25.0,<3.0.0" grpc-google-iam-v1 = ">=0.14.0,<1.0.0" proto-plus = [ - {version = ">=1.22.3,<2.0.0"}, {version = ">=1.25.0,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.22.3,<2.0.0"}, ] protobuf = ">=3.20.2,<4.21.0 || >4.21.0,<4.21.1 || >4.21.1,<4.21.2 || >4.21.2,<4.21.3 || >4.21.3,<4.21.4 || >4.21.4,<4.21.5 || >4.21.5,<7.0.0" @@ -3648,6 +3658,7 @@ groups = ["main", "evaluation"] files = [ {file = "grpclib-0.4.7.tar.gz", hash = "sha256:2988ef57c02b22b7a2e8e961792c41ccf97efc2ace91ae7a5b0de03c363823c3"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [package.dependencies] h2 = ">=3.1.0,<5" @@ -3710,6 +3721,7 @@ files = [ {file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"}, {file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [package.dependencies] hpack = ">=4.1,<5" @@ -3748,6 +3760,7 @@ files = [ {file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"}, {file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [[package]] name = "html2text" @@ -3885,6 +3898,7 @@ files = [ {file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"}, {file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [[package]] name = "identify" @@ -5389,6 +5403,7 @@ files = [ {file = "marshmallow-3.26.1-py3-none-any.whl", hash = "sha256:3350409f20a70a7e4e11a27661187b77cdcaeb20abca41c1454fe33636bea09c"}, {file = "marshmallow-3.26.1.tar.gz", hash = "sha256:e6d8affb6cb61d39d26402096dc0aee12d5a26d490a121f118d2e81dc0719dc6"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [package.dependencies] packaging = ">=17.0" @@ -5580,6 +5595,7 @@ files = [ {file = "modal-1.0.4-py3-none-any.whl", hash = "sha256:6c0d96bb49b09fa47e407a13e49545e32fe0803803b4330fbeb38de5e71209cc"}, {file = "modal-1.0.4.tar.gz", hash = "sha256:09a575ff5fcae1e690b10187bea6da7ff01430c38ec1785090bf7a7ccee7f408"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [package.dependencies] aiohttp = "*" @@ -6586,8 +6602,8 @@ files = [ [package.dependencies] googleapis-common-protos = ">=1.52,<2.0" grpcio = [ - {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""}, {version = ">=1.66.2,<2.0.0", markers = "python_version >= \"3.13\""}, + {version = ">=1.63.2,<2.0.0", markers = "python_version < \"3.13\""}, ] opentelemetry-api = ">=1.15,<2.0" opentelemetry-exporter-otlp-proto-common = "1.34.1" @@ -9056,9 +9072,10 @@ files = [ name = "runloop-api-client" version = "0.43.0" description = "The official Python library for the runloop API" -optional = false +optional = true python-versions = ">=3.8" groups = ["main"] +markers = "extra == \"third-party-runtimes\"" files = [ {file = "runloop_api_client-0.43.0-py3-none-any.whl", hash = "sha256:20b6098b8e0714bb48812a97d5f420f547a98748d52d90789d60a38fa37a2526"}, {file = "runloop_api_client-0.43.0.tar.gz", hash = "sha256:879ee6a3baaabd7fd9930fe0c187de8458d138afea4f50c1e428cbf73f2ef08a"}, @@ -9350,7 +9367,6 @@ files = [ {file = "setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922"}, {file = "setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c"}, ] -markers = {evaluation = "platform_system == \"Linux\" and platform_machine == \"x86_64\""} [package.extras] check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.8.0) ; sys_platform != \"cygwin\""] @@ -9442,6 +9458,7 @@ files = [ {file = "sigtools-4.0.1-py2.py3-none-any.whl", hash = "sha256:d216b4cf920bbab0fce636ddc429ed8463a5b533d9e1492acb45a2a1bc36ac6c"}, {file = "sigtools-4.0.1.tar.gz", hash = "sha256:4b8e135a9cd4d2ea00da670c093372d74e672ba3abb87f4c98d8e73dea54445c"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [package.dependencies] attrs = "*" @@ -9593,7 +9610,7 @@ description = "Standard library aifc redistribution. \"dead battery\"." optional = false python-versions = "*" groups = ["main"] -markers = "python_version >= \"3.13\"" +markers = "python_version == \"3.13\"" files = [ {file = "standard_aifc-3.13.0-py3-none-any.whl", hash = "sha256:f7ae09cc57de1224a0dd8e3eb8f73830be7c3d0bc485de4c1f82b4a7f645ac66"}, {file = "standard_aifc-3.13.0.tar.gz", hash = "sha256:64e249c7cb4b3daf2fdba4e95721f811bde8bdfc43ad9f936589b7bb2fae2e43"}, @@ -9610,7 +9627,7 @@ description = "Standard library chunk redistribution. \"dead battery\"." optional = false python-versions = "*" groups = ["main"] -markers = "python_version >= \"3.13\"" +markers = "python_version == \"3.13\"" files = [ {file = "standard_chunk-3.13.0-py3-none-any.whl", hash = "sha256:17880a26c285189c644bd5bd8f8ed2bdb795d216e3293e6dbe55bbd848e2982c"}, {file = "standard_chunk-3.13.0.tar.gz", hash = "sha256:4ac345d37d7e686d2755e01836b8d98eda0d1a3ee90375e597ae43aaf064d654"}, @@ -9798,6 +9815,7 @@ files = [ {file = "synchronicity-0.9.15-py3-none-any.whl", hash = "sha256:6e3008f54795d73d59fbd133c812734e7c83f4a6f44257cc2a3251237ee8921b"}, {file = "synchronicity-0.9.15.tar.gz", hash = "sha256:9451d0caef3509e9f980ba62885a3b8ba7ab247845618e9d9c9c8d11da7ee84b"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [package.dependencies] sigtools = ">=4.0.1" @@ -10455,6 +10473,7 @@ 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"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [[package]] name = "types-python-dateutil" @@ -10843,6 +10862,7 @@ files = [ {file = "watchfiles-1.0.5-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0901429650652d3f0da90bad42bdafc1f9143ff3605633c455c999a2d786cac"}, {file = "watchfiles-1.0.5.tar.gz", hash = "sha256:b7529b5dcc114679d43827d8c35a07c493ad6f083633d573d81c660abc5979e9"}, ] +markers = {main = "extra == \"third-party-runtimes\""} [package.dependencies] anyio = ">=3.0.0" @@ -11769,7 +11789,10 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\ [package.extras] cffi = ["cffi (>=1.11)"] +[extras] +third-party-runtimes = ["daytona", "e2b", "modal", "runloop-api-client"] + [metadata] lock-version = "2.1" python-versions = "^3.12,<3.14" -content-hash = "a2cf9e6529f7ed81f96c6183607aa61f99293027bbd3b4a635733f7c3c8e52cb" +content-hash = "653c4cda22ec5ff95420d305386c53ba440714fe7c59a9f7f240fdf86b698031" diff --git a/pyproject.toml b/pyproject.toml index 19ebf11359..6418d02988 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,18 +14,19 @@ readme = "README.md" repository = "https://github.com/All-Hands-AI/OpenHands" packages = [ { include = "openhands/**/*" }, + { include = "third_party/**/*" }, { include = "pyproject.toml", to = "openhands" }, { include = "poetry.lock", to = "openhands" }, ] [tool.poetry.dependencies] python = "^3.12,<3.14" -litellm = "^1.60.0, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272) -aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13 -google-generativeai = "*" # To use litellm with Gemini Pro API -google-api-python-client = "^2.164.0" # For Google Sheets API -google-auth-httplib2 = "*" # For Google Sheets authentication -google-auth-oauthlib = "*" # For Google Sheets OAuth +litellm = "^1.60.0, !=1.64.4, !=1.67.*" # avoid 1.64.4 (known bug) & 1.67.* (known bug #10272) +aiohttp = ">=3.9.0,!=3.11.13" # Pin to avoid yanked version 3.11.13 +google-generativeai = "*" # To use litellm with Gemini Pro API +google-api-python-client = "^2.164.0" # For Google Sheets API +google-auth-httplib2 = "*" # For Google Sheets authentication +google-auth-oauthlib = "*" # For Google Sheets OAuth termcolor = "*" docker = "*" fastapi = "*" @@ -34,9 +35,9 @@ types-toml = "*" uvicorn = "*" numpy = "*" json-repair = "*" -browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface +browsergym-core = "0.13.3" # integrate browsergym-core as the browsing interface html2text = "*" -e2b = ">=1.0.5,<1.6.0" + pexpect = "*" jinja2 = "^3.1.3" python-multipart = "*" @@ -52,8 +53,7 @@ whatthepatch = "^1.0.6" protobuf = "^5.0.0,<6.0.0" # Updated to support newer opentelemetry opentelemetry-api = "^1.33.1" opentelemetry-exporter-otlp-proto-grpc = "^1.33.1" -modal = ">=0.66.26,<1.1.0" -runloop-api-client = "0.43.0" + libtmux = ">=0.37,<0.40" pygithub = "^2.5.0" joblib = "*" @@ -80,7 +80,7 @@ bashlex = "^0.18" # TODO: These are integrations that should probably be optional redis = ">=5.2,<7.0" minio = "^7.2.8" -daytona = "0.21.1" + stripe = ">=11.5,<13.0" google-cloud-aiplatform = "*" anthropic = { extras = [ "vertex" ], version = "*" } @@ -88,6 +88,15 @@ boto3 = "*" kubernetes = "^33.1.0" pyyaml = "^6.0.2" +# Third-party runtime dependencies (optional) +e2b = { version = ">=1.0.5,<1.6.0", optional = true } +modal = { version = ">=0.66.26,<1.1.0", optional = true } +runloop-api-client = { version = "0.43.0", optional = true } +daytona = { version = "0.21.1", optional = true } + +[tool.poetry.extras] +third_party_runtimes = [ "e2b", "modal", "runloop-api-client", "daytona" ] + [tool.poetry.group.dev] optional = true diff --git a/tests/runtime/conftest.py b/tests/runtime/conftest.py index 96483ccd01..0e3ba32f91 100644 --- a/tests/runtime/conftest.py +++ b/tests/runtime/conftest.py @@ -12,11 +12,9 @@ from openhands.core.logger import openhands_logger as logger from openhands.events import EventStream from openhands.runtime.base import Runtime from openhands.runtime.impl.cli.cli_runtime import CLIRuntime -from openhands.runtime.impl.daytona.daytona_runtime import DaytonaRuntime from openhands.runtime.impl.docker.docker_runtime import DockerRuntime from openhands.runtime.impl.local.local_runtime import LocalRuntime from openhands.runtime.impl.remote.remote_runtime import RemoteRuntime -from openhands.runtime.impl.runloop.runloop_runtime import RunloopRuntime from openhands.runtime.plugins import AgentSkillsRequirement, JupyterRequirement from openhands.storage import get_file_store from openhands.utils.async_utils import call_async_from_sync @@ -130,10 +128,6 @@ def get_runtime_classes() -> list[type[Runtime]]: return [LocalRuntime] elif runtime.lower() == 'remote': return [RemoteRuntime] - elif runtime.lower() == 'runloop': - return [RunloopRuntime] - elif runtime.lower() == 'daytona': - return [DaytonaRuntime] elif runtime.lower() == 'cli': return [CLIRuntime] else: diff --git a/tests/unit/test_config.py b/tests/unit/test_config.py index 52ea452c6f..bb500dcacd 100644 --- a/tests/unit/test_config.py +++ b/tests/unit/test_config.py @@ -990,34 +990,15 @@ def test_api_keys_repr_str(): app_config = OpenHandsConfig( llms={'llm': llm_config}, agents={'agent': agent_config}, - e2b_api_key='my_e2b_api_key', - jwt_secret='my_jwt_secret', - modal_api_token_id='my_modal_api_token_id', - modal_api_token_secret='my_modal_api_token_secret', - runloop_api_key='my_runloop_api_key', - daytona_api_key='my_daytona_api_key', + search_api_key='my_search_api_key', ) - assert 'my_e2b_api_key' not in repr(app_config) - assert 'my_e2b_api_key' not in str(app_config) - assert 'my_jwt_secret' not in repr(app_config) - assert 'my_jwt_secret' not in str(app_config) - assert 'my_modal_api_token_id' not in repr(app_config) - assert 'my_modal_api_token_id' not in str(app_config) - assert 'my_modal_api_token_secret' not in repr(app_config) - assert 'my_modal_api_token_secret' not in str(app_config) - assert 'my_runloop_api_key' not in repr(app_config) - assert 'my_runloop_api_key' not in str(app_config) - assert 'my_daytona_api_key' not in repr(app_config) - assert 'my_daytona_api_key' not in str(app_config) + + assert 'my_search_api_key' not in repr(app_config) + assert 'my_search_api_key' not in str(app_config) # Check that no other attrs in OpenHandsConfig have 'key' or 'token' in their name # This will fail when new attrs are added, and attract attention known_key_token_attrs_app = [ - 'e2b_api_key', - 'modal_api_token_id', - 'modal_api_token_secret', - 'runloop_api_key', - 'daytona_api_key', 'search_api_key', ] for attr_name in OpenHandsConfig.model_fields.keys(): diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index 7055f69b18..8c65c338e1 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -84,11 +84,11 @@ def test_llm_config_attributes_masking(test_handler): def test_app_config_attributes_masking(test_handler): logger, stream = test_handler - app_config = OpenHandsConfig(e2b_api_key='e2b-xyz789') + app_config = OpenHandsConfig(search_api_key='search-xyz789') logger.info(f'App Config: {app_config}') log_output = stream.getvalue() assert 'github_token' not in log_output - assert 'e2b-xyz789' not in log_output + assert 'search-xyz789' not in log_output assert 'ghp_abcdefghijklmnopqrstuvwxyz' not in log_output diff --git a/third_party/__init__.py b/third_party/__init__.py new file mode 100644 index 0000000000..fb4b6e3669 --- /dev/null +++ b/third_party/__init__.py @@ -0,0 +1,14 @@ +"""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 +""" \ No newline at end of file diff --git a/containers/e2b-sandbox/Dockerfile b/third_party/containers/e2b-sandbox/Dockerfile similarity index 100% rename from containers/e2b-sandbox/Dockerfile rename to third_party/containers/e2b-sandbox/Dockerfile diff --git a/containers/e2b-sandbox/README.md b/third_party/containers/e2b-sandbox/README.md similarity index 100% rename from containers/e2b-sandbox/README.md rename to third_party/containers/e2b-sandbox/README.md diff --git a/containers/e2b-sandbox/e2b.toml b/third_party/containers/e2b-sandbox/e2b.toml similarity index 100% rename from containers/e2b-sandbox/e2b.toml rename to third_party/containers/e2b-sandbox/e2b.toml diff --git a/third_party/runtime/__init__.py b/third_party/runtime/__init__.py new file mode 100644 index 0000000000..c5cdfaaa5c --- /dev/null +++ b/third_party/runtime/__init__.py @@ -0,0 +1 @@ +"""Third-party runtime implementations.""" \ No newline at end of file diff --git a/third_party/runtime/impl/__init__.py b/third_party/runtime/impl/__init__.py new file mode 100644 index 0000000000..27f1695b76 --- /dev/null +++ b/third_party/runtime/impl/__init__.py @@ -0,0 +1 @@ +"""Third-party runtime implementation modules.""" \ No newline at end of file diff --git a/openhands/runtime/impl/daytona/README.md b/third_party/runtime/impl/daytona/README.md similarity index 100% rename from openhands/runtime/impl/daytona/README.md rename to third_party/runtime/impl/daytona/README.md diff --git a/third_party/runtime/impl/daytona/__init__.py b/third_party/runtime/impl/daytona/__init__.py new file mode 100644 index 0000000000..467302bdbb --- /dev/null +++ b/third_party/runtime/impl/daytona/__init__.py @@ -0,0 +1,7 @@ +"""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') +""" \ No newline at end of file diff --git a/openhands/runtime/impl/daytona/daytona_runtime.py b/third_party/runtime/impl/daytona/daytona_runtime.py similarity index 94% rename from openhands/runtime/impl/daytona/daytona_runtime.py rename to third_party/runtime/impl/daytona/daytona_runtime.py index 82e6f5bd57..631946c63d 100644 --- a/openhands/runtime/impl/daytona/daytona_runtime.py +++ b/third_party/runtime/impl/daytona/daytona_runtime.py @@ -1,3 +1,4 @@ +import os from typing import Callable import httpx @@ -45,7 +46,13 @@ class DaytonaRuntime(ActionExecutionClient): user_id: str | None = None, git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None, ): - assert config.daytona_api_key, 'Daytona API key is required' + # 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 @@ -53,9 +60,9 @@ class DaytonaRuntime(ActionExecutionClient): self._vscode_url: str | None = None daytona_config = DaytonaConfig( - api_key=config.daytona_api_key.get_secret_value(), - server_url=config.daytona_api_url, - target=config.daytona_target, + api_key=daytona_api_key, + server_url=daytona_api_url, + target=daytona_target, ) self.daytona = Daytona(daytona_config) diff --git a/openhands/runtime/impl/e2b/README.md b/third_party/runtime/impl/e2b/README.md similarity index 100% rename from openhands/runtime/impl/e2b/README.md rename to third_party/runtime/impl/e2b/README.md diff --git a/third_party/runtime/impl/e2b/__init__.py b/third_party/runtime/impl/e2b/__init__.py new file mode 100644 index 0000000000..2cbb9a612e --- /dev/null +++ b/third_party/runtime/impl/e2b/__init__.py @@ -0,0 +1,5 @@ +"""E2B runtime implementation. + +This runtime reads configuration directly from environment variables: +- E2B_API_KEY: API key for E2B authentication +""" \ No newline at end of file diff --git a/openhands/runtime/impl/e2b/e2b_runtime.py b/third_party/runtime/impl/e2b/e2b_runtime.py similarity index 93% rename from openhands/runtime/impl/e2b/e2b_runtime.py rename to third_party/runtime/impl/e2b/e2b_runtime.py index 29a0249e22..d32f47ee10 100644 --- a/openhands/runtime/impl/e2b/e2b_runtime.py +++ b/third_party/runtime/impl/e2b/e2b_runtime.py @@ -16,8 +16,8 @@ from openhands.integrations.provider import PROVIDER_TOKEN_TYPE from openhands.runtime.impl.action_execution.action_execution_client import ( ActionExecutionClient, ) -from openhands.runtime.impl.e2b.filestore import E2BFileStore -from openhands.runtime.impl.e2b.sandbox import E2BSandbox +from third_party.runtime.impl.e2b.filestore import E2BFileStore +from third_party.runtime.impl.e2b.sandbox import E2BSandbox from openhands.runtime.plugins import PluginRequirement from openhands.runtime.utils.files import insert_lines, read_lines @@ -50,7 +50,7 @@ class E2BRuntime(ActionExecutionClient): git_provider_tokens, ) if sandbox is None: - self.sandbox = E2BSandbox() + self.sandbox = E2BSandbox(config.sandbox) if not isinstance(self.sandbox, E2BSandbox): raise ValueError('E2BRuntime requires an E2BSandbox') self.file_store = E2BFileStore(self.sandbox.filesystem) diff --git a/openhands/runtime/impl/e2b/filestore.py b/third_party/runtime/impl/e2b/filestore.py similarity index 100% rename from openhands/runtime/impl/e2b/filestore.py rename to third_party/runtime/impl/e2b/filestore.py diff --git a/openhands/runtime/impl/e2b/sandbox.py b/third_party/runtime/impl/e2b/sandbox.py similarity index 93% rename from openhands/runtime/impl/e2b/sandbox.py rename to third_party/runtime/impl/e2b/sandbox.py index 44c015d6f6..d48529243c 100644 --- a/openhands/runtime/impl/e2b/sandbox.py +++ b/third_party/runtime/impl/e2b/sandbox.py @@ -19,11 +19,16 @@ class E2BBox: def __init__( self, config: SandboxConfig, - e2b_api_key: str, template: str = 'openhands', ): 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') + self.sandbox = E2BSandbox( api_key=e2b_api_key, template=template, @@ -112,3 +117,7 @@ class E2BBox: 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 new file mode 100644 index 0000000000..8ab3aa7fac --- /dev/null +++ b/third_party/runtime/impl/modal/__init__.py @@ -0,0 +1,6 @@ +"""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 +""" \ No newline at end of file diff --git a/openhands/runtime/impl/modal/modal_runtime.py b/third_party/runtime/impl/modal/modal_runtime.py similarity index 95% rename from openhands/runtime/impl/modal/modal_runtime.py rename to third_party/runtime/impl/modal/modal_runtime.py index 6ce8277156..dcc4814aa2 100644 --- a/openhands/runtime/impl/modal/modal_runtime.py +++ b/third_party/runtime/impl/modal/modal_runtime.py @@ -57,16 +57,22 @@ class ModalRuntime(ActionExecutionClient): user_id: str | None = None, git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None, ): - assert config.modal_api_token_id, 'Modal API token id is required' - assert config.modal_api_token_secret, 'Modal API token secret is required' + # 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( - config.modal_api_token_id.get_secret_value(), - config.modal_api_token_secret.get_secret_value(), + modal_token_id, + modal_token_secret, ) self.app = modal.App.lookup( 'openhands', create_if_missing=True, client=self.modal_client diff --git a/openhands/runtime/impl/runloop/README.md b/third_party/runtime/impl/runloop/README.md similarity index 100% rename from openhands/runtime/impl/runloop/README.md rename to third_party/runtime/impl/runloop/README.md diff --git a/third_party/runtime/impl/runloop/__init__.py b/third_party/runtime/impl/runloop/__init__.py new file mode 100644 index 0000000000..a775d72587 --- /dev/null +++ b/third_party/runtime/impl/runloop/__init__.py @@ -0,0 +1,5 @@ +"""Runloop runtime implementation. + +This runtime reads configuration directly from environment variables: +- RUNLOOP_API_KEY: API key for Runloop authentication +""" \ No newline at end of file diff --git a/openhands/runtime/impl/runloop/runloop_runtime.py b/third_party/runtime/impl/runloop/runloop_runtime.py similarity index 95% rename from openhands/runtime/impl/runloop/runloop_runtime.py rename to third_party/runtime/impl/runloop/runloop_runtime.py index 00b684d489..b3e37b143d 100644 --- a/openhands/runtime/impl/runloop/runloop_runtime.py +++ b/third_party/runtime/impl/runloop/runloop_runtime.py @@ -1,4 +1,5 @@ import logging +import os from typing import Callable import tenacity @@ -40,11 +41,15 @@ class RunloopRuntime(ActionExecutionClient): user_id: str | None = None, git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None, ): - assert config.runloop_api_key is not None, 'Runloop API key is required' + # 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=config.runloop_api_key.get_secret_value(), + bearer_token=runloop_api_key, ) self.container_name = CONTAINER_NAME_PREFIX + sid super().__init__(