Compare commits

...

13 Commits

Author SHA1 Message Date
Engel Nyst
da4b96da71 Fix test_config.py for daytona_api_key
- Add daytona_api_key to known_key_token_attrs_app whitelist
- Add test assertions to verify daytona_api_key is properly hidden in repr/str methods
- Ensures daytona_api_key follows same security patterns as other API keys
2025-06-23 14:33:34 +02:00
Engel Nyst
d9937ce566 fix lint 2025-06-23 13:43:16 +02:00
Engel Nyst
2cfdebfbe2 fix lock 2025-06-23 13:37:02 +02:00
Engel Nyst
9d5b39da98 Restore Daytona runtime integration with extras-based installation
- Add extras section to pyproject.toml with 'daytona' and 'all-runtimes' options
- Add optional daytona dependency (version 0.21.1) to pyproject.toml
- Restore Daytona config fields to openhands_config.py (daytona_api_key, daytona_api_url, daytona_target)
- Implement conditional Daytona runtime registration in openhands/runtime/__init__.py
- Add daytona_api_key to sensitive patterns list in openhands/core/logger.py
- Update tests/runtime/conftest.py with conditional Daytona runtime support
- Restore Daytona runtime files from git history:
  - openhands/runtime/impl/daytona/daytona_runtime.py
  - openhands/runtime/impl/daytona/README.md
  - docs/usage/runtimes/daytona.mdx
- Update docs/usage/runtimes/overview.mdx to include Daytona with extras installation note
- Update Daytona documentation with installation instructions for extras system
- Modify containers/app/Dockerfile to install all-runtimes extras by default

This approach keeps Daytona in the main repo but makes it optional via pip extras:
- Users can install with: pip install openhands-ai[daytona] or pip install openhands-ai[all-runtimes]
- Default Docker build includes Daytona (via all-runtimes extra)
- Conditional imports prevent errors when daytona package is not installed
- Runtime registration is dynamic based on package availability
2025-06-23 13:33:38 +02:00
Engel Nyst
caeeb0356e Merge branch 'main' of https://github.com/All-Hands-AI/OpenHands into openhands-workspace-ze101m8y 2025-06-23 13:32:22 +02:00
Graham Neubig
1e237d0bd9 Update docs/usage/runtimes/overview.mdx
Co-authored-by: Engel Nyst <enyst@users.noreply.github.com>
2025-06-20 09:44:35 -07:00
Graham Neubig
5fda84f8cc Fix 2025-06-20 05:47:11 -07:00
Graham Neubig
9589e6655c Delete tests/unit/test_third_party_runtimes_removal.py 2025-06-20 05:11:49 -07:00
openhands
b319bea288 docs: remove third-party runtime references from docstring
- Remove E2BRuntime, ModalRuntime, and DaytonaRuntime from Runtime class docstring
- Update docstring to reflect current available runtimes
- Add KubernetesRuntime and CLIRuntime to documentation
2025-06-20 00:23:19 +00:00
openhands
a3e02662d3 Merge main branch and resolve conflicts
Resolved merge conflicts after removing third-party runtimes:
- openhands/runtime/__init__.py: Added Kubernetes runtime, removed third-party runtimes
- pyproject.toml: Removed modal and runloop-api-client dependencies
- poetry.lock: Regenerated to match updated dependencies

The merge brings in latest changes from main including:
- Kubernetes runtime support
- File upload functionality
- Budget tracking features
- FAQ documentation
- Various bug fixes and improvements

All third-party runtime removals are preserved while incorporating new features.
2025-06-20 00:16:00 +00:00
openhands
033dceb8ee Fix failing tests after third-party runtime removal
- Update test_api_keys_repr_str to remove references to removed third-party runtime API keys (e2b_api_key, modal_api_token_id, modal_api_token_secret, runloop_api_key, daytona_api_key)
- Update test_app_config_attributes_masking to use search_api_key instead of removed e2b_api_key
- Both tests now pass and still verify the core functionality of API key masking for remaining configuration fields
2025-06-18 16:48:54 +00:00
openhands
09bf498cd5 Update poetry.lock after removing third-party dependencies
The poetry.lock file was out of sync after removing third-party runtime
dependencies (e2b, modal, runloop-api-client, daytona) from pyproject.toml.
This was causing CI failures due to dependency resolution issues.
2025-06-18 14:59:08 +00:00
openhands
1087173bdf Remove third-party runtimes (daytona, modal, e2b, runloop) from main codebase
- Remove runtime implementation directories: openhands/runtime/impl/{daytona,modal,e2b,runloop}/
- Remove e2b-sandbox container directory: containers/e2b-sandbox/
- Update runtime imports and registrations in __init__.py files
- Remove third-party dependencies from pyproject.toml (e2b, modal, runloop-api-client, daytona)
- Remove config fields from openhands_config.py (API keys, URLs)
- Update tests/runtime/conftest.py to remove third-party runtime references
- Remove documentation files for third-party runtimes
- Update docs/usage/runtimes/overview.mdx to remove third-party runtime references
- Remove API key references from openhands/core/logger.py
- Add comprehensive test suite to verify complete removal

Fixes #9212
2025-06-18 12:15:14 +00:00
27 changed files with 114 additions and 1063 deletions

View File

@@ -26,7 +26,7 @@ RUN apt-get update -y \
COPY pyproject.toml poetry.lock ./
RUN touch README.md
RUN export POETRY_CACHE_DIR && poetry install --no-root && rm -rf $POETRY_CACHE_DIR
RUN export POETRY_CACHE_DIR && poetry install --no-root --extras all-runtimes && rm -rf $POETRY_CACHE_DIR
FROM base AS openhands-app

View File

@@ -1,19 +0,0 @@
FROM ubuntu:24.04
# install basic packages
RUN apt-get update && apt-get install -y \
curl \
wget \
git \
vim \
nano \
unzip \
zip \
python3 \
python3-pip \
python3-venv \
python3-dev \
build-essential \
openssh-server \
sudo \
&& rm -rf /var/lib/apt/lists/*

View File

@@ -1,15 +0,0 @@
# How to build custom E2B sandbox for OpenHands
[E2B](https://e2b.dev) is an [open-source](https://github.com/e2b-dev/e2b) secure cloud environment (sandbox) made for running AI-generated code and agents. E2B offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b) SDK to spawn and control these sandboxes.
1. Install the CLI with NPM.
```sh
npm install -g @e2b/cli@latest
```
Full CLI API is [here](https://e2b.dev/docs/cli/installation).
1. Build the sandbox
```sh
e2b template build --dockerfile ./Dockerfile --name "openhands"
```

View File

@@ -1,14 +0,0 @@
# This is a config for E2B sandbox template.
# You can use 'template_id' (785n69crgahmz0lkdw9h) or 'template_name (openhands) from this config to spawn a sandbox:
# Python SDK
# from e2b import Sandbox
# sandbox = Sandbox(template='openhands')
# JS SDK
# import { Sandbox } from 'e2b'
# const sandbox = await Sandbox.create({ template: 'openhands' })
dockerfile = "Dockerfile"
template_name = "openhands"
template_id = "785n69crgahmz0lkdw9h"

View File

@@ -7,22 +7,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`

View File

@@ -3,6 +3,19 @@ title: Daytona Runtime
description: You can use [Daytona](https://www.daytona.io/) as a runtime provider.
---
## Installation
The Daytona runtime is available as an optional runtime. To use it, install OpenHands with the Daytona extra:
```bash
pip install openhands-ai[daytona]
```
Or to install all available runtimes:
```bash
pip install openhands-ai[all-runtimes]
```
## Step 1: Retrieve Your Daytona API Key
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).

View File

@@ -1,38 +0,0 @@
---
title: E2B Runtime
description: E2B is an open-source secure cloud environment (sandbox) made for running AI-generated code and agents.
---
[E2B](https://e2b.dev) 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. and it's called `openhands`.
## Debugging
You can connect to a running E2B sandbox with E2B CLI in your terminal.
- List all running sandboxes (based on your API key)
```sh
e2b sandbox list
```
- Connect to a running sandbox
```sh
e2b sandbox connect <sandbox-id>
```
## Links
- [E2B Docs](https://e2b.dev/docs)
- [E2B GitHub](https://github.com/e2b-dev/e2b)

View File

@@ -1,14 +0,0 @@
---
title: Modal Runtime
---
Our partners at [Modal](https://modal.com/) have provided a runtime for OpenHands.
To use the Modal Runtime, create an account, and then [create an API key.](https://modal.com/settings)
You'll then need to set the following environment variables when starting OpenHands:
```bash
docker run # ...
-e RUNTIME=modal \
-e MODAL_API_TOKEN_ID="your-id" \
-e MODAL_API_TOKEN_SECRET="modal-api-key" \
```

View File

@@ -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,5 @@ 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.
- [Daytona Runtime](/usage/runtimes/daytona) - Runtime provided by Daytona.
- [Third-party Runtimes](https://github.com/All-Hands-AI/third-party-runtimes) - These runtimes are supported by their developers, not by OpenHands. Please find them in the repository linked here if you would like to run on third-party infrastructure providers.

View File

@@ -1,31 +0,0 @@
---
title: Runloop Runtime
description: Runloop provides a fast, secure and scalable AI sandbox (Devbox). Check out the [runloop docs](https://docs.runloop.ai/overview/what-is-runloop) for more detail.
---
## Access
Runloop is currently available in a closed beta. For early access, or
just to say hello, sign up at https://www.runloop.ai/hello
## Set up
With your runloop API,
```bash
export RUNLOOP_API_KEY=<your-api-key>
```
Configure the runtime
```bash
export RUNTIME="runloop"
```
## Interact with your devbox
Runloop provides additional tools to interact with your Devbox based
runtime environment. See the [docs](https://docs.runloop.ai/tools) for an up
to date list of tools.
### Dashboard
View logs, ssh into, or view your Devbox status from the [dashboard](https://platform.runloop.ai)
### CLI
Use the Runloop CLI to view logs, execute commands, and more.
See the setup instructions [here](https://docs.runloop.ai/tools/cli)

View File

@@ -45,7 +45,7 @@ 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.
@@ -87,19 +87,17 @@ 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)

View File

@@ -254,11 +254,8 @@ class SensitiveDataFilter(logging.Filter):
'api_key',
'aws_access_key_id',
'aws_secret_access_key',
'e2b_api_key',
'github_token',
'jwt_secret',
'modal_api_token_id',
'modal_api_token_secret',
'llm_api_key',
'sandbox_env_github_token',
'daytona_api_key',

View File

@@ -1,31 +1,35 @@
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
# Conditionally import Daytona runtime if available
try:
from openhands.runtime.impl.daytona.daytona_runtime import DaytonaRuntime
_DAYTONA_AVAILABLE = True
except ImportError:
_DAYTONA_AVAILABLE = False
# 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,
}
# Add Daytona runtime if available
if _DAYTONA_AVAILABLE:
_DEFAULT_RUNTIME_CLASSES['daytona'] = DaytonaRuntime
def get_runtime_cls(name: str) -> type[Runtime]:
"""
@@ -46,13 +50,13 @@ def get_runtime_cls(name: str) -> type[Runtime]:
__all__ = [
'Runtime',
'E2BRuntime',
'RemoteRuntime',
'ModalRuntime',
'RunloopRuntime',
'DockerRuntime',
'DaytonaRuntime',
'KubernetesRuntime',
'CLIRuntime',
'get_runtime_cls',
]
# Add DaytonaRuntime to exports if available
if _DAYTONA_AVAILABLE:
__all__.append('DaytonaRuntime')

View File

@@ -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

View File

@@ -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',
]

View File

@@ -1,35 +0,0 @@
# How to use E2B
[E2B](https://e2b.dev) is an [open-source](https://github.com/e2b-dev/e2b) secure cloud environment (sandbox) made for running AI-generated code and agents. E2B offers [Python](https://pypi.org/project/e2b/) and [JS/TS](https://www.npmjs.com/package/e2b) SDK to spawn and control these sandboxes.
## Getting started
1. [Get your API key](https://e2b.dev/docs/getting-started/api-key)
1. Set your E2B API key to the `E2B_API_KEY` env var when starting the Docker container
1. **Optional** - Install the CLI with NPM.
```sh
npm install -g @e2b/cli@latest
```
Full CLI API is [here](https://e2b.dev/docs/cli/installation).
## OpenHands sandbox
You can use the E2B CLI to create a custom sandbox with a Dockerfile. Read the full guide [here](https://e2b.dev/docs/guide/custom-sandbox). The premade OpenHands sandbox for E2B is set up in the [`containers` directory](/containers/e2b-sandbox). and it's called `openhands`.
## Debugging
You can connect to a running E2B sandbox with E2B CLI in your terminal.
- List all running sandboxes (based on your API key)
```sh
e2b sandbox list
```
- Connect to a running sandbox
```sh
e2b sandbox connect <sandbox-id>
```
## Links
- [E2B Docs](https://e2b.dev/docs)
- [E2B GitHub](https://github.com/e2b-dev/e2b)

View File

@@ -1,78 +0,0 @@
from typing import Callable
from openhands.core.config import OpenHandsConfig
from openhands.events.action import (
FileReadAction,
FileWriteAction,
)
from openhands.events.observation import (
ErrorObservation,
FileReadObservation,
FileWriteObservation,
Observation,
)
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.impl.e2b.filestore import E2BFileStore
from openhands.runtime.impl.e2b.sandbox import E2BSandbox
from openhands.runtime.plugins import PluginRequirement
from openhands.runtime.utils.files import insert_lines, read_lines
class E2BRuntime(ActionExecutionClient):
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,
sandbox: E2BSandbox | None = None,
):
super().__init__(
config,
event_stream,
sid,
plugins,
env_vars,
status_callback,
attach_to_existing,
headless_mode,
user_id,
git_provider_tokens,
)
if sandbox is None:
self.sandbox = E2BSandbox()
if not isinstance(self.sandbox, E2BSandbox):
raise ValueError('E2BRuntime requires an E2BSandbox')
self.file_store = E2BFileStore(self.sandbox.filesystem)
def read(self, action: FileReadAction) -> Observation:
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)
def write(self, action: FileWriteAction) -> Observation:
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:
# FIXME: we should create a new file here
return ErrorObservation(f'File not found: {action.path}')

View File

@@ -1,27 +0,0 @@
from typing import Protocol
from openhands.storage.files import FileStore
class SupportsFilesystemOperations(Protocol):
def write(self, path: str, contents: str | bytes) -> None: ...
def read(self, path: str) -> str: ...
def list(self, path: str) -> list[str]: ...
def delete(self, path: str) -> None: ...
class E2BFileStore(FileStore):
def __init__(self, filesystem: SupportsFilesystemOperations) -> None:
self.filesystem = filesystem
def write(self, path: str, contents: str | bytes) -> None:
self.filesystem.write(path, contents)
def read(self, path: str) -> str:
return self.filesystem.read(path)
def list(self, path: str) -> list[str]:
return self.filesystem.list(path)
def delete(self, path: str) -> None:
self.filesystem.delete(path)

View File

@@ -1,114 +0,0 @@
import copy
import os
import tarfile
from glob import glob
from e2b import Sandbox as E2BSandbox
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,
e2b_api_key: str,
template: str = 'openhands',
):
self.config = copy.deepcopy(config)
self.initialize_plugins: bool = config.initialize_plugins
self.sandbox = E2BSandbox(
api_key=e2b_api_key,
template=template,
# It's possible to stream stdout and stderr from sandbox and from each process
on_stderr=lambda x: logger.debug(f'E2B sandbox stderr: {x}'),
on_stdout=lambda x: logger.debug(f'E2B sandbox stdout: {x}'),
cwd=self._cwd, # Default workdir inside sandbox
)
logger.debug(f'Started E2B sandbox with ID "{self.sandbox.id}"')
@property
def filesystem(self):
return self.sandbox.filesystem
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
process = self.sandbox.process.start(cmd, env_vars=self._env)
try:
process_output = process.wait(timeout=timeout)
except TimeoutException:
logger.debug('Command timed out, killing process...')
process.kill()
return -1, f'Command: "{cmd}" timed out'
logs = [m.line for m in process_output.messages]
logs_str = '\n'.join(logs)
if process.exit_code is None:
return -1, logs_str
assert process_output.exit_code is not None
return process_output.exit_code, logs_str
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.
process = self.sandbox.process.start_and_wait(f'test -d {sandbox_dest}')
if process.exit_code != 0:
self.sandbox.filesystem.make_dir(sandbox_dest)
# Extract the archive into the destination and delete the archive
process = self.sandbox.process.start_and_wait(
f'sudo tar -xf {uploaded_path} -C {sandbox_dest} && sudo rm {uploaded_path}'
)
if process.exit_code != 0:
raise Exception(
f'Failed to extract {uploaded_path} to {sandbox_dest}: {process.stderr}'
)
# Delete the local archive
os.remove(tar_filename)
def close(self):
self.sandbox.close()
def get_working_directory(self):
return self.sandbox.cwd

View File

@@ -1,288 +0,0 @@
import os
import tempfile
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,
):
assert config.modal_api_token_id, 'Modal API token id is required'
assert config.modal_api_token_secret, 'Modal API token secret is required'
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(),
)
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('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,
)
base_runtime_image = modal.Image.from_dockerfile(
path=os.path.join(build_folder, 'Dockerfile'),
context_mount=modal.Mount.from_local_dir(
local_path=build_folder,
remote_path='.', # to current WORKDIR
),
)
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', 'Container started')
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

View File

@@ -1,31 +0,0 @@
# Runloop Runtime
Runloop provides a fast, secure and scalable AI sandbox (Devbox).
Check out the [runloop docs](https://docs.runloop.ai/overview/what-is-runloop)
for more detail
## Access
Runloop is currently available in a closed beta. For early access, or
just to say hello, sign up at https://www.runloop.ai/hello
## Set up
With your runloop API,
```bash
export RUNLOOP_API_KEY=<your-api-key>
```
Configure the runtime
```bash
export RUNTIME="runloop"
```
## Interact with your devbox
Runloop provides additional tools to interact with your Devbox based
runtime environment. See the [docs](https://docs.runloop.ai/tools) for an up
to date list of tools.
### Dashboard
View logs, ssh into, or view your Devbox status from the [dashboard](https://platform.runloop.ai)
### CLI
Use the Runloop CLI to view logs, execute commands, and more.
See the setup instructions [here](https://docs.runloop.ai/tools/cli)

View File

@@ -1,198 +0,0 @@
import logging
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,
):
assert config.runloop_api_key is not None, 'Runloop API key is required'
self.devbox: DevboxView | None = None
self.config = config
self.runloop_api_client = Runloop(
bearer_token=config.runloop_api_key.get_secret_value(),
)
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

112
poetry.lock generated
View File

@@ -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 == \"daytona\" or extra == \"all-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 == \"daytona\" or extra == \"all-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 == \"daytona\" or extra == \"all-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 == \"daytona\" or extra == \"all-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 == \"daytona\" or extra == \"all-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 == \"daytona\" or extra == \"all-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 == \"daytona\" or extra == \"all-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 == \"daytona\" or extra == \"all-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"},
@@ -2313,27 +2321,6 @@ files = [
{file = "durationpy-0.10.tar.gz", hash = "sha256:1fa6893409a6e739c9c72334fc65cca1f355dbdd93405d30f726deb5bde42fba"},
]
[[package]]
name = "e2b"
version = "1.5.2"
description = "E2B SDK that give agents cloud environments"
optional = false
python-versions = "<4.0,>=3.9"
groups = ["main"]
files = [
{file = "e2b-1.5.2-py3-none-any.whl", hash = "sha256:8cf755f2ff04098daa7ac778f768eee1df730a6181637fe124210345999890b3"},
{file = "e2b-1.5.2.tar.gz", hash = "sha256:29ed891ae04ffafff1744c57eff55901200f15030d34ac3fe76d6672e2bf7845"},
]
[package.dependencies]
attrs = ">=23.2.0"
httpcore = ">=1.0.5,<2.0.0"
httpx = ">=0.27.0,<1.0.0"
packaging = ">=24.1"
protobuf = ">=5.29.4,<6.0.0"
python-dateutil = ">=2.8.2"
typing-extensions = ">=4.1.0"
[[package]]
name = "english-words"
version = "2.0.1"
@@ -2349,9 +2336,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 == \"daytona\" or extra == \"all-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 +3041,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 +3064,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 +3283,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"
@@ -3644,7 +3632,7 @@ version = "0.4.7"
description = "Pure-Python gRPC implementation for asyncio"
optional = false
python-versions = ">=3.7"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "grpclib-0.4.7.tar.gz", hash = "sha256:2988ef57c02b22b7a2e8e961792c41ccf97efc2ace91ae7a5b0de03c363823c3"},
]
@@ -3705,7 +3693,7 @@ version = "4.2.0"
description = "Pure-Python HTTP/2 protocol implementation"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0"},
{file = "h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f"},
@@ -3743,7 +3731,7 @@ version = "4.1.0"
description = "Pure-Python HPACK header encoding"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496"},
{file = "hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca"},
@@ -3880,7 +3868,7 @@ version = "6.1.0"
description = "Pure-Python HTTP/2 framing"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5"},
{file = "hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08"},
@@ -5389,6 +5377,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 == \"daytona\" or extra == \"all-runtimes\""}
[package.dependencies]
packaging = ">=17.0"
@@ -5575,7 +5564,7 @@ version = "1.0.4"
description = "Python client library for Modal"
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "modal-1.0.4-py3-none-any.whl", hash = "sha256:6c0d96bb49b09fa47e407a13e49545e32fe0803803b4330fbeb38de5e71209cc"},
{file = "modal-1.0.4.tar.gz", hash = "sha256:09a575ff5fcae1e690b10187bea6da7ff01430c38ec1785090bf7a7ccee7f408"},
@@ -6586,8 +6575,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"
@@ -9052,26 +9041,6 @@ files = [
{file = "ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c"},
]
[[package]]
name = "runloop-api-client"
version = "0.43.0"
description = "The official Python library for the runloop API"
optional = false
python-versions = ">=3.8"
groups = ["main"]
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"},
]
[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]]
name = "s3transfer"
version = "0.11.3"
@@ -9350,7 +9319,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\""]
@@ -9437,7 +9405,7 @@ version = "4.0.1"
description = "Utilities for working with inspect.Signature objects."
optional = false
python-versions = ">=3.6"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "sigtools-4.0.1-py2.py3-none-any.whl", hash = "sha256:d216b4cf920bbab0fce636ddc429ed8463a5b533d9e1492acb45a2a1bc36ac6c"},
{file = "sigtools-4.0.1.tar.gz", hash = "sha256:4b8e135a9cd4d2ea00da670c093372d74e672ba3abb87f4c98d8e73dea54445c"},
@@ -9593,7 +9561,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 +9578,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"},
@@ -9793,7 +9761,7 @@ version = "0.9.15"
description = "Export blocking and async library versions from a single async implementation"
optional = false
python-versions = ">=3.8"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "synchronicity-0.9.15-py3-none-any.whl", hash = "sha256:6e3008f54795d73d59fbd133c812734e7c83f4a6f44257cc2a3251237ee8921b"},
{file = "synchronicity-0.9.15.tar.gz", hash = "sha256:9451d0caef3509e9f980ba62885a3b8ba7ab247845618e9d9c9c8d11da7ee84b"},
@@ -10450,7 +10418,7 @@ version = "2021.10.8.3"
description = "Typing stubs for certifi"
optional = false
python-versions = "*"
groups = ["main", "evaluation"]
groups = ["evaluation"]
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"},
@@ -10769,7 +10737,7 @@ version = "1.0.5"
description = "Simple, modern and high performance file watching and code reload in python."
optional = false
python-versions = ">=3.9"
groups = ["main", "evaluation"]
groups = ["evaluation"]
files = [
{file = "watchfiles-1.0.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:5c40fe7dd9e5f81e0847b1ea64e1f5dd79dd61afbedb57759df06767ac719b40"},
{file = "watchfiles-1.0.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8c0db396e6003d99bb2d7232c957b5f0b5634bbd1b24e381a5afcc880f7373fb"},
@@ -11769,7 +11737,11 @@ cffi = {version = ">=1.11", markers = "platform_python_implementation == \"PyPy\
[package.extras]
cffi = ["cffi (>=1.11)"]
[extras]
all-runtimes = ["daytona"]
daytona = ["daytona"]
[metadata]
lock-version = "2.1"
python-versions = "^3.12,<3.14"
content-hash = "a2cf9e6529f7ed81f96c6183607aa61f99293027bbd3b4a635733f7c3c8e52cb"
content-hash = "10b6f924cad187d42cd854a8d067e61e6286cda25fbdf1058f780ae4f5e9d078"

View File

@@ -20,12 +20,12 @@ packages = [
[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 +34,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 +52,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 +79,8 @@ 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"
daytona = { version = "0.21.1", optional = true }
stripe = ">=11.5,<13.0"
google-cloud-aiplatform = "*"
anthropic = { extras = [ "vertex" ], version = "*" }
@@ -150,6 +150,10 @@ pyarrow = "20.0.0" #
datasets = "*"
joblib = "*"
[tool.poetry.extras]
daytona = [ "daytona" ]
all-runtimes = [ "daytona" ]
[tool.poetry.scripts]
openhands = "openhands.cli.main:main"

View File

@@ -12,11 +12,17 @@ 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
# Conditionally import Daytona runtime if available
try:
from openhands.runtime.impl.daytona.daytona_runtime import DaytonaRuntime
_DAYTONA_AVAILABLE = True
except ImportError:
_DAYTONA_AVAILABLE = False
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 +136,13 @@ 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]
if _DAYTONA_AVAILABLE:
return [DaytonaRuntime]
else:
raise ValueError(
'Daytona runtime not available. Install with: pip install openhands-ai[daytona]'
)
elif runtime.lower() == 'cli':
return [CLIRuntime]
else:

View File

@@ -990,35 +990,19 @@ 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',
search_api_key='my_search_api_key',
daytona_api_key='my_daytona_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_search_api_key' not in repr(app_config)
assert 'my_search_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)
# 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',
'daytona_api_key',
]
for attr_name in OpenHandsConfig.model_fields.keys():
if (

View File

@@ -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