mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-10 15:28:14 -05:00
Remove third-party runtimes (daytona, modal, e2b, runloop) from main codebase (#9213)
Co-authored-by: openhands <openhands@all-hands.dev> Co-authored-by: Engel Nyst <enyst@users.noreply.github.com> Co-authored-by: Engel Nyst <engel.nyst@gmail.com>
This commit is contained in:
@@ -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',
|
||||
]
|
||||
|
||||
@@ -1,134 +0,0 @@
|
||||
# Daytona Runtime
|
||||
|
||||
[Daytona](https://www.daytona.io/) is a platform that provides a secure and elastic infrastructure for running AI-generated code. It provides all the necessary features for an AI Agent to interact with a codebase. It provides a Daytona SDK with official Python and TypeScript interfaces for interacting with Daytona, enabling you to programmatically manage development environments and execute code.
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Step 1: Retrieve Your Daytona API Key
|
||||
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
|
||||
2. Click **"Create Key"**.
|
||||
3. Enter a name for your key and confirm the creation.
|
||||
4. Once the key is generated, copy it.
|
||||
|
||||
### Step 2: Set Your API Key as an Environment Variable
|
||||
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
|
||||
|
||||
Mac/Linux:
|
||||
```bash
|
||||
export DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
Windows PowerShell:
|
||||
```powershell
|
||||
$env:DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
This step ensures that OpenHands can authenticate with the Daytona platform when it runs.
|
||||
|
||||
### Step 3: Run OpenHands Locally Using Docker
|
||||
To start the latest version of OpenHands on your machine, execute the following command in your terminal:
|
||||
|
||||
Mac/Linux:
|
||||
```bash
|
||||
bash -i <(curl -sL https://get.daytona.io/openhands)
|
||||
```
|
||||
|
||||
Windows:
|
||||
```powershell
|
||||
powershell -Command "irm https://get.daytona.io/openhands-windows | iex"
|
||||
```
|
||||
|
||||
#### What This Command Does:
|
||||
- Downloads the latest OpenHands release script.
|
||||
- Runs the script in an interactive Bash session.
|
||||
- Automatically pulls and runs the OpenHands container using Docker.
|
||||
Once executed, OpenHands should be running locally and ready for use.
|
||||
|
||||
|
||||
## Manual Initialization
|
||||
|
||||
### Step 1: Set the `OPENHANDS_VERSION` Environment Variable
|
||||
Run the following command in your terminal, replacing `<openhands-release>` with the latest release's version seen in the [main README.md file](https://github.com/All-Hands-AI/OpenHands?tab=readme-ov-file#-quick-start):
|
||||
|
||||
#### Mac/Linux:
|
||||
```bash
|
||||
export OPENHANDS_VERSION="<openhands-release>" # e.g. 0.27
|
||||
```
|
||||
|
||||
#### Windows PowerShell:
|
||||
```powershell
|
||||
$env:OPENHANDS_VERSION="<openhands-release>" # e.g. 0.27
|
||||
```
|
||||
|
||||
### Step 2: Retrieve Your Daytona API Key
|
||||
1. Visit the [Daytona Dashboard](https://app.daytona.io/dashboard/keys).
|
||||
2. Click **"Create Key"**.
|
||||
3. Enter a name for your key and confirm the creation.
|
||||
4. Once the key is generated, copy it.
|
||||
|
||||
### Step 3: Set Your API Key as an Environment Variable:
|
||||
Run the following command in your terminal, replacing `<your-api-key>` with the actual key you copied:
|
||||
|
||||
#### Mac/Linux:
|
||||
```bash
|
||||
export DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
#### Windows PowerShell:
|
||||
```powershell
|
||||
$env:DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
### Step 4: Run the following `docker` command:
|
||||
This command pulls and runs the OpenHands container using Docker. Once executed, OpenHands should be running locally and ready for use.
|
||||
|
||||
#### Mac/Linux:
|
||||
```bash
|
||||
docker run -it --rm --pull=always \
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:${OPENHANDS_VERSION}-nikolaik \
|
||||
-e LOG_ALL_EVENTS=true \
|
||||
-e RUNTIME=daytona \
|
||||
-e DAYTONA_API_KEY=${DAYTONA_API_KEY} \
|
||||
-v ~/.openhands:/.openhands \
|
||||
-p 3000:3000 \
|
||||
--name openhands-app \
|
||||
docker.all-hands.dev/all-hands-ai/openhands:${OPENHANDS_VERSION}
|
||||
```
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
|
||||
#### Windows:
|
||||
```powershell
|
||||
docker run -it --rm --pull=always `
|
||||
-e SANDBOX_RUNTIME_CONTAINER_IMAGE=docker.all-hands.dev/all-hands-ai/runtime:${env:OPENHANDS_VERSION}-nikolaik `
|
||||
-e LOG_ALL_EVENTS=true `
|
||||
-e RUNTIME=daytona `
|
||||
-e DAYTONA_API_KEY=${env:DAYTONA_API_KEY} `
|
||||
-v ~/.openhands:/.openhands `
|
||||
-p 3000:3000 `
|
||||
--name openhands-app `
|
||||
docker.all-hands.dev/all-hands-ai/openhands:${env:OPENHANDS_VERSION}
|
||||
```
|
||||
|
||||
> **Note**: If you used OpenHands before version 0.44, you may want to run `mv ~/.openhands-state ~/.openhands` to migrate your conversation history to the new location.
|
||||
|
||||
> **Tip:** If you don't want your sandboxes to default to the EU region, you can set the `DAYTONA_TARGET` environment variable to `us`
|
||||
|
||||
### Running OpenHands Locally Without Docker
|
||||
|
||||
Alternatively, if you want to run the OpenHands app on your local machine using `make run` without Docker, make sure to set the following environment variables first:
|
||||
|
||||
#### Mac/Linux:
|
||||
```bash
|
||||
export RUNTIME="daytona"
|
||||
export DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
#### Windows PowerShell:
|
||||
```powershell
|
||||
$env:RUNTIME="daytona"
|
||||
$env:DAYTONA_API_KEY="<your-api-key>"
|
||||
```
|
||||
|
||||
## Documentation
|
||||
Read more by visiting our [documentation](https://www.daytona.io/docs/) page.
|
||||
@@ -1,275 +0,0 @@
|
||||
from typing import Callable
|
||||
|
||||
import httpx
|
||||
import tenacity
|
||||
from daytona import (
|
||||
CreateSandboxFromSnapshotParams,
|
||||
Daytona,
|
||||
DaytonaConfig,
|
||||
Sandbox,
|
||||
SessionExecuteRequest,
|
||||
)
|
||||
|
||||
from openhands.core.config.openhands_config import OpenHandsConfig
|
||||
from openhands.events.stream import EventStream
|
||||
from openhands.integrations.provider import PROVIDER_TOKEN_TYPE
|
||||
from openhands.runtime.impl.action_execution.action_execution_client import (
|
||||
ActionExecutionClient,
|
||||
)
|
||||
from openhands.runtime.plugins.requirement import PluginRequirement
|
||||
from openhands.runtime.runtime_status import RuntimeStatus
|
||||
from openhands.runtime.utils.command import get_action_execution_server_startup_command
|
||||
from openhands.runtime.utils.request import RequestHTTPError
|
||||
from openhands.utils.async_utils import call_sync_from_async
|
||||
from openhands.utils.tenacity_stop import stop_if_should_exit
|
||||
|
||||
OPENHANDS_SID_LABEL = 'OpenHands_SID'
|
||||
|
||||
|
||||
class DaytonaRuntime(ActionExecutionClient):
|
||||
"""The DaytonaRuntime class is a DockerRuntime that utilizes Daytona Sandboxes as runtime environments."""
|
||||
|
||||
_sandbox_port: int = 4444
|
||||
_vscode_port: int = 4445
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: OpenHandsConfig,
|
||||
event_stream: EventStream,
|
||||
sid: str = 'default',
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
attach_to_existing: bool = False,
|
||||
headless_mode: bool = True,
|
||||
user_id: str | None = None,
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
|
||||
):
|
||||
assert config.daytona_api_key, 'Daytona API key is required'
|
||||
|
||||
self.config = config
|
||||
self.sid = sid
|
||||
self.sandbox: Sandbox | None = None
|
||||
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,
|
||||
)
|
||||
self.daytona = Daytona(daytona_config)
|
||||
|
||||
# workspace_base cannot be used because we can't bind mount into a workspace.
|
||||
if self.config.workspace_base is not None:
|
||||
self.log(
|
||||
'warning',
|
||||
'Workspace mounting is not supported in the Daytona runtime.',
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
config,
|
||||
event_stream,
|
||||
sid,
|
||||
plugins,
|
||||
env_vars,
|
||||
status_callback,
|
||||
attach_to_existing,
|
||||
headless_mode,
|
||||
user_id,
|
||||
git_provider_tokens,
|
||||
)
|
||||
|
||||
def _get_sandbox(self) -> Sandbox | None:
|
||||
try:
|
||||
sandboxes = self.daytona.list({OPENHANDS_SID_LABEL: self.sid})
|
||||
if len(sandboxes) == 0:
|
||||
return None
|
||||
assert len(sandboxes) == 1, 'Multiple sandboxes found for SID'
|
||||
|
||||
sandbox = sandboxes[0]
|
||||
|
||||
self.log('info', f'Attached to existing sandbox with id: {self.sid}')
|
||||
except Exception:
|
||||
self.log(
|
||||
'warning',
|
||||
f'Failed to attach to existing sandbox with id: {self.sid}',
|
||||
)
|
||||
sandbox = None
|
||||
|
||||
return sandbox
|
||||
|
||||
def _get_creation_env_vars(self) -> dict[str, str]:
|
||||
env_vars: dict[str, str] = {
|
||||
'port': str(self._sandbox_port),
|
||||
'PYTHONUNBUFFERED': '1',
|
||||
'VSCODE_PORT': str(self._vscode_port),
|
||||
}
|
||||
|
||||
if self.config.debug:
|
||||
env_vars['DEBUG'] = 'true'
|
||||
|
||||
return env_vars
|
||||
|
||||
def _create_sandbox(self) -> Sandbox:
|
||||
sandbox_params = CreateSandboxFromSnapshotParams(
|
||||
language='python',
|
||||
snapshot=self.config.sandbox.runtime_container_image,
|
||||
public=True,
|
||||
env_vars=self._get_creation_env_vars(),
|
||||
labels={OPENHANDS_SID_LABEL: self.sid},
|
||||
)
|
||||
return self.daytona.create(sandbox_params)
|
||||
|
||||
def _construct_api_url(self, port: int) -> str:
|
||||
assert self.sandbox is not None, 'Sandbox is not initialized'
|
||||
assert self.sandbox.runner_domain is not None, 'Runner domain is not available'
|
||||
|
||||
return f'https://{port}-{self.sandbox.id}.{self.sandbox.runner_domain}'
|
||||
|
||||
@property
|
||||
def action_execution_server_url(self) -> str:
|
||||
return self.api_url
|
||||
|
||||
def _start_action_execution_server(self) -> None:
|
||||
assert self.sandbox is not None, 'Sandbox is not initialized'
|
||||
|
||||
start_command: list[str] = get_action_execution_server_startup_command(
|
||||
server_port=self._sandbox_port,
|
||||
plugins=self.plugins,
|
||||
app_config=self.config,
|
||||
override_user_id=1000,
|
||||
override_username='openhands',
|
||||
)
|
||||
start_command_str: str = (
|
||||
f'mkdir -p {self.config.workspace_mount_path_in_sandbox} && cd /openhands/code && '
|
||||
+ ' '.join(start_command)
|
||||
)
|
||||
|
||||
self.log(
|
||||
'debug',
|
||||
f'Starting action execution server with command: {start_command_str}',
|
||||
)
|
||||
|
||||
exec_session_id = 'action-execution-server'
|
||||
self.sandbox.process.create_session(exec_session_id)
|
||||
|
||||
exec_command = self.sandbox.process.execute_session_command(
|
||||
exec_session_id,
|
||||
SessionExecuteRequest(command=start_command_str, var_async=True),
|
||||
)
|
||||
|
||||
self.log('debug', f'exec_command_id: {exec_command.cmd_id}')
|
||||
|
||||
@tenacity.retry(
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
wait=tenacity.wait_fixed(1),
|
||||
reraise=(ConnectionRefusedError,),
|
||||
)
|
||||
def _wait_until_alive(self):
|
||||
super().check_if_alive()
|
||||
|
||||
async def connect(self):
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
should_start_action_execution_server = False
|
||||
|
||||
if self.attach_to_existing:
|
||||
self.sandbox = await call_sync_from_async(self._get_sandbox)
|
||||
else:
|
||||
should_start_action_execution_server = True
|
||||
|
||||
if self.sandbox is None:
|
||||
self.set_runtime_status(RuntimeStatus.BUILDING_RUNTIME)
|
||||
self.sandbox = await call_sync_from_async(self._create_sandbox)
|
||||
self.log('info', f'Created a new sandbox with id: {self.sid}')
|
||||
|
||||
self.api_url = self._construct_api_url(self._sandbox_port)
|
||||
|
||||
state = self.sandbox.state
|
||||
|
||||
if state == 'stopping':
|
||||
self.log('info', 'Waiting for the Daytona sandbox to stop...')
|
||||
await call_sync_from_async(self.sandbox.wait_for_sandbox_stop)
|
||||
state = 'stopped'
|
||||
|
||||
if state == 'stopped':
|
||||
self.log('info', 'Starting the Daytona sandbox...')
|
||||
await call_sync_from_async(self.sandbox.start)
|
||||
should_start_action_execution_server = True
|
||||
|
||||
if should_start_action_execution_server:
|
||||
await call_sync_from_async(self._start_action_execution_server)
|
||||
self.log(
|
||||
'info',
|
||||
f'Container started. Action execution server url: {self.api_url}',
|
||||
)
|
||||
|
||||
self.log('info', 'Waiting for client to become ready...')
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
await call_sync_from_async(self._wait_until_alive)
|
||||
|
||||
if should_start_action_execution_server:
|
||||
await call_sync_from_async(self.setup_initial_env)
|
||||
|
||||
self.log(
|
||||
'info',
|
||||
f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}',
|
||||
)
|
||||
|
||||
if should_start_action_execution_server:
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
self._runtime_initialized = True
|
||||
|
||||
@tenacity.retry(
|
||||
retry=tenacity.retry_if_exception(
|
||||
lambda e: (
|
||||
isinstance(e, httpx.HTTPError) or isinstance(e, RequestHTTPError)
|
||||
)
|
||||
and hasattr(e, 'response')
|
||||
and e.response.status_code == 502
|
||||
),
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
wait=tenacity.wait_fixed(1),
|
||||
reraise=True,
|
||||
)
|
||||
def _send_action_server_request(self, method, url, **kwargs):
|
||||
return super()._send_action_server_request(method, url, **kwargs)
|
||||
|
||||
def close(self):
|
||||
super().close()
|
||||
|
||||
if self.attach_to_existing:
|
||||
return
|
||||
|
||||
if self.sandbox:
|
||||
self.sandbox.delete()
|
||||
|
||||
@property
|
||||
def vscode_url(self) -> str | None:
|
||||
if self._vscode_url is not None: # cached value
|
||||
return self._vscode_url
|
||||
token = super().get_vscode_token()
|
||||
if not token:
|
||||
self.log(
|
||||
'warning', 'Failed to get VSCode token while trying to get VSCode URL'
|
||||
)
|
||||
return None
|
||||
if not self.sandbox:
|
||||
self.log(
|
||||
'warning', 'Sandbox is not initialized while trying to get VSCode URL'
|
||||
)
|
||||
return None
|
||||
self._vscode_url = (
|
||||
self._construct_api_url(self._vscode_port)
|
||||
+ f'/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
|
||||
)
|
||||
|
||||
self.log(
|
||||
'debug',
|
||||
f'VSCode URL: {self._vscode_url}',
|
||||
)
|
||||
|
||||
return self._vscode_url
|
||||
|
||||
@property
|
||||
def additional_agent_instructions(self) -> str:
|
||||
return f'When showing endpoints to access applications for any port, e.g. port 3000, instead of localhost:3000, use this format: {self._construct_api_url(3000)}.'
|
||||
@@ -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)
|
||||
@@ -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}')
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,31 +0,0 @@
|
||||
# Runloop Runtime
|
||||
Runloop provides a fast, secure and scalable AI sandbox (Devbox).
|
||||
Check out the [runloop docs](https://docs.runloop.ai/overview/what-is-runloop)
|
||||
for more detail
|
||||
|
||||
## Access
|
||||
Runloop is currently available in a closed beta. For early access, or
|
||||
just to say hello, sign up at https://www.runloop.ai/hello
|
||||
|
||||
## Set up
|
||||
With your runloop API,
|
||||
```bash
|
||||
export RUNLOOP_API_KEY=<your-api-key>
|
||||
```
|
||||
|
||||
Configure the runtime
|
||||
```bash
|
||||
export RUNTIME="runloop"
|
||||
```
|
||||
|
||||
## Interact with your devbox
|
||||
Runloop provides additional tools to interact with your Devbox based
|
||||
runtime environment. See the [docs](https://docs.runloop.ai/tools) for an up
|
||||
to date list of tools.
|
||||
|
||||
### Dashboard
|
||||
View logs, ssh into, or view your Devbox status from the [dashboard](https://platform.runloop.ai)
|
||||
|
||||
### CLI
|
||||
Use the Runloop CLI to view logs, execute commands, and more.
|
||||
See the setup instructions [here](https://docs.runloop.ai/tools/cli)
|
||||
@@ -1,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
|
||||
Reference in New Issue
Block a user