mirror of
https://github.com/All-Hands-AI/OpenHands.git
synced 2026-01-09 14:57:59 -05:00
chore(lint): Apply comprehensive linting and formatting fixes (#10287)
Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
4
third_party/__init__.py
vendored
4
third_party/__init__.py
vendored
@@ -8,7 +8,7 @@ To use third-party runtimes, install OpenHands with the third_party_runtimes ext
|
||||
|
||||
Available third-party runtimes:
|
||||
- daytona: Daytona cloud development environment
|
||||
- e2b: E2B secure sandbox environment
|
||||
- e2b: E2B secure sandbox environment
|
||||
- modal: Modal cloud compute platform
|
||||
- runloop: Runloop AI sandbox environment
|
||||
"""
|
||||
"""
|
||||
|
||||
2
third_party/runtime/__init__.py
vendored
2
third_party/runtime/__init__.py
vendored
@@ -1 +1 @@
|
||||
"""Third-party runtime implementations."""
|
||||
"""Third-party runtime implementations."""
|
||||
|
||||
2
third_party/runtime/impl/__init__.py
vendored
2
third_party/runtime/impl/__init__.py
vendored
@@ -1 +1 @@
|
||||
"""Third-party runtime implementation modules."""
|
||||
"""Third-party runtime implementation modules."""
|
||||
|
||||
2
third_party/runtime/impl/daytona/__init__.py
vendored
2
third_party/runtime/impl/daytona/__init__.py
vendored
@@ -4,4 +4,4 @@ This runtime reads configuration directly from environment variables:
|
||||
- DAYTONA_API_KEY: API key for Daytona authentication
|
||||
- DAYTONA_API_URL: Daytona API URL endpoint (defaults to https://app.daytona.io/api)
|
||||
- DAYTONA_TARGET: Daytona target region (defaults to 'eu')
|
||||
"""
|
||||
"""
|
||||
|
||||
100
third_party/runtime/impl/daytona/daytona_runtime.py
vendored
100
third_party/runtime/impl/daytona/daytona_runtime.py
vendored
@@ -24,7 +24,7 @@ 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'
|
||||
OPENHANDS_SID_LABEL = "OpenHands_SID"
|
||||
|
||||
|
||||
class DaytonaRuntime(ActionExecutionClient):
|
||||
@@ -37,7 +37,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
self,
|
||||
config: OpenHandsConfig,
|
||||
event_stream: EventStream,
|
||||
sid: str = 'default',
|
||||
sid: str = "default",
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
@@ -47,11 +47,13 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
|
||||
):
|
||||
# Read Daytona configuration from environment variables
|
||||
daytona_api_key = os.getenv('DAYTONA_API_KEY')
|
||||
daytona_api_key = os.getenv("DAYTONA_API_KEY")
|
||||
if not daytona_api_key:
|
||||
raise ValueError('DAYTONA_API_KEY environment variable is required for Daytona runtime')
|
||||
daytona_api_url = os.getenv('DAYTONA_API_URL', 'https://app.daytona.io/api')
|
||||
daytona_target = os.getenv('DAYTONA_TARGET', 'eu')
|
||||
raise ValueError(
|
||||
"DAYTONA_API_KEY environment variable is required for Daytona runtime"
|
||||
)
|
||||
daytona_api_url = os.getenv("DAYTONA_API_URL", "https://app.daytona.io/api")
|
||||
daytona_target = os.getenv("DAYTONA_TARGET", "eu")
|
||||
|
||||
self.config = config
|
||||
self.sid = sid
|
||||
@@ -68,8 +70,8 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
# 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.',
|
||||
"warning",
|
||||
"Workspace mounting is not supported in the Daytona runtime.",
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
@@ -90,15 +92,15 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
sandboxes = self.daytona.list({OPENHANDS_SID_LABEL: self.sid})
|
||||
if len(sandboxes) == 0:
|
||||
return None
|
||||
assert len(sandboxes) == 1, 'Multiple sandboxes found for SID'
|
||||
assert len(sandboxes) == 1, "Multiple sandboxes found for SID"
|
||||
|
||||
sandbox = sandboxes[0]
|
||||
|
||||
self.log('info', f'Attached to existing sandbox with id: {self.sid}')
|
||||
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}',
|
||||
"warning",
|
||||
f"Failed to attach to existing sandbox with id: {self.sid}",
|
||||
)
|
||||
sandbox = None
|
||||
|
||||
@@ -106,23 +108,25 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
|
||||
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),
|
||||
"port": str(self._sandbox_port),
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"VSCODE_PORT": str(self._vscode_port),
|
||||
}
|
||||
|
||||
if self.config.debug:
|
||||
env_vars['DEBUG'] = 'true'
|
||||
env_vars["DEBUG"] = "true"
|
||||
|
||||
return env_vars
|
||||
|
||||
def _create_sandbox(self) -> Sandbox:
|
||||
# Check if auto-stop should be disabled - otherwise have it trigger after 60 minutes
|
||||
disable_auto_stop = os.getenv('DAYTONA_DISABLE_AUTO_STOP', 'false').lower() == 'true'
|
||||
disable_auto_stop = (
|
||||
os.getenv("DAYTONA_DISABLE_AUTO_STOP", "false").lower() == "true"
|
||||
)
|
||||
auto_stop_interval = 0 if disable_auto_stop else 60
|
||||
|
||||
sandbox_params = CreateSandboxFromSnapshotParams(
|
||||
language='python',
|
||||
language="python",
|
||||
snapshot=self.config.sandbox.runtime_container_image,
|
||||
public=True,
|
||||
env_vars=self._get_creation_env_vars(),
|
||||
@@ -132,7 +136,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
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 is not None, "Sandbox is not initialized"
|
||||
return self.sandbox.get_preview_link(port).url
|
||||
|
||||
@property
|
||||
@@ -140,26 +144,26 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
return self.api_url
|
||||
|
||||
def _start_action_execution_server(self) -> None:
|
||||
assert self.sandbox is not None, 'Sandbox is not initialized'
|
||||
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',
|
||||
override_username="openhands",
|
||||
)
|
||||
start_command_str: str = (
|
||||
f'mkdir -p {self.config.workspace_mount_path_in_sandbox} && cd /openhands/code && '
|
||||
+ ' '.join(start_command)
|
||||
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}',
|
||||
"debug",
|
||||
f"Starting action execution server with command: {start_command_str}",
|
||||
)
|
||||
|
||||
exec_session_id = 'action-execution-server'
|
||||
exec_session_id = "action-execution-server"
|
||||
self.sandbox.process.create_session(exec_session_id)
|
||||
|
||||
exec_command = self.sandbox.process.execute_session_command(
|
||||
@@ -167,7 +171,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
SessionExecuteRequest(command=start_command_str, var_async=True),
|
||||
)
|
||||
|
||||
self.log('debug', f'exec_command_id: {exec_command.cmd_id}')
|
||||
self.log("debug", f"exec_command_id: {exec_command.cmd_id}")
|
||||
|
||||
@tenacity.retry(
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
@@ -189,30 +193,30 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
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.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...')
|
||||
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'
|
||||
state = "stopped"
|
||||
|
||||
if state == 'stopped':
|
||||
self.log('info', 'Starting the Daytona sandbox...')
|
||||
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}',
|
||||
"info",
|
||||
f"Container started. Action execution server url: {self.api_url}",
|
||||
)
|
||||
|
||||
self.log('info', 'Waiting for client to become ready...')
|
||||
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)
|
||||
|
||||
@@ -220,8 +224,8 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
await call_sync_from_async(self.setup_initial_env)
|
||||
|
||||
self.log(
|
||||
'info',
|
||||
f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}',
|
||||
"info",
|
||||
f"Container initialized with plugins: {[plugin.name for plugin in self.plugins]}",
|
||||
)
|
||||
|
||||
if should_start_action_execution_server:
|
||||
@@ -233,7 +237,7 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
lambda e: (
|
||||
isinstance(e, httpx.HTTPError) or isinstance(e, RequestHTTPError)
|
||||
)
|
||||
and hasattr(e, 'response')
|
||||
and hasattr(e, "response")
|
||||
and e.response.status_code == 502
|
||||
),
|
||||
stop=tenacity.stop_after_delay(120) | stop_if_should_exit(),
|
||||
@@ -250,13 +254,15 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
return
|
||||
|
||||
if self.sandbox:
|
||||
delete_on_close = os.getenv('DAYTONA_DELETE_ON_CLOSE', 'false').lower() == 'true'
|
||||
delete_on_close = (
|
||||
os.getenv("DAYTONA_DELETE_ON_CLOSE", "false").lower() == "true"
|
||||
)
|
||||
|
||||
if delete_on_close:
|
||||
self.sandbox.delete()
|
||||
else:
|
||||
# Only stop if sandbox is currently started
|
||||
if self._get_sandbox().state == 'started':
|
||||
if self._get_sandbox().state == "started":
|
||||
self.sandbox.stop()
|
||||
|
||||
@property
|
||||
@@ -266,26 +272,26 @@ class DaytonaRuntime(ActionExecutionClient):
|
||||
token = super().get_vscode_token()
|
||||
if not token:
|
||||
self.log(
|
||||
'warning', 'Failed to get VSCode token while trying to get VSCode URL'
|
||||
"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'
|
||||
"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}'
|
||||
+ f"/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}"
|
||||
)
|
||||
|
||||
self.log(
|
||||
'debug',
|
||||
f'VSCode URL: {self._vscode_url}',
|
||||
"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)}.'
|
||||
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)}."
|
||||
|
||||
2
third_party/runtime/impl/e2b/__init__.py
vendored
2
third_party/runtime/impl/e2b/__init__.py
vendored
@@ -2,4 +2,4 @@
|
||||
|
||||
This runtime reads configuration directly from environment variables:
|
||||
- E2B_API_KEY: API key for E2B authentication
|
||||
"""
|
||||
"""
|
||||
|
||||
20
third_party/runtime/impl/e2b/e2b_runtime.py
vendored
20
third_party/runtime/impl/e2b/e2b_runtime.py
vendored
@@ -27,7 +27,7 @@ class E2BRuntime(ActionExecutionClient):
|
||||
self,
|
||||
config: OpenHandsConfig,
|
||||
event_stream: EventStream,
|
||||
sid: str = 'default',
|
||||
sid: str = "default",
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
@@ -52,27 +52,27 @@ class E2BRuntime(ActionExecutionClient):
|
||||
if sandbox is None:
|
||||
self.sandbox = E2BSandbox(config.sandbox)
|
||||
if not isinstance(self.sandbox, E2BSandbox):
|
||||
raise ValueError('E2BRuntime requires an 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)
|
||||
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)
|
||||
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')
|
||||
all_lines = self.file_store.read(action.path).split("\n")
|
||||
new_file = insert_lines(
|
||||
action.content.split('\n'), all_lines, action.start, action.end
|
||||
action.content.split("\n"), all_lines, action.start, action.end
|
||||
)
|
||||
self.file_store.write(action.path, ''.join(new_file))
|
||||
return FileWriteObservation('', path=action.path)
|
||||
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}')
|
||||
return ErrorObservation(f"File not found: {action.path}")
|
||||
|
||||
46
third_party/runtime/impl/e2b/sandbox.py
vendored
46
third_party/runtime/impl/e2b/sandbox.py
vendored
@@ -12,29 +12,31 @@ from openhands.core.logger import openhands_logger as logger
|
||||
|
||||
class E2BBox:
|
||||
closed = False
|
||||
_cwd: str = '/home/user'
|
||||
_cwd: str = "/home/user"
|
||||
_env: dict[str, str] = {}
|
||||
is_initial_session: bool = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
config: SandboxConfig,
|
||||
template: str = 'openhands',
|
||||
template: str = "openhands",
|
||||
):
|
||||
self.config = copy.deepcopy(config)
|
||||
self.initialize_plugins: bool = config.initialize_plugins
|
||||
|
||||
|
||||
# Read API key from environment variable
|
||||
e2b_api_key = os.getenv('E2B_API_KEY')
|
||||
e2b_api_key = os.getenv("E2B_API_KEY")
|
||||
if not e2b_api_key:
|
||||
raise ValueError('E2B_API_KEY environment variable is required for E2B runtime')
|
||||
|
||||
raise ValueError(
|
||||
"E2B_API_KEY environment variable is required for E2B runtime"
|
||||
)
|
||||
|
||||
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}'),
|
||||
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}"')
|
||||
@@ -46,23 +48,23 @@ class E2BBox:
|
||||
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'
|
||||
"Source must be a directory when recursive is True"
|
||||
)
|
||||
files = glob(host_src + '/**/*', recursive=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:
|
||||
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'
|
||||
"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_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
|
||||
|
||||
@@ -72,12 +74,12 @@ class E2BBox:
|
||||
try:
|
||||
process_output = process.wait(timeout=timeout)
|
||||
except TimeoutException:
|
||||
logger.debug('Command timed out, killing process...')
|
||||
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)
|
||||
logs_str = "\n".join(logs)
|
||||
if process.exit_code is None:
|
||||
return -1, logs_str
|
||||
|
||||
@@ -89,24 +91,24 @@ class E2BBox:
|
||||
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('/'))
|
||||
sandbox_dest = os.path.join(self._cwd, sandbox_dest.removeprefix("/"))
|
||||
|
||||
with open(tar_filename, 'rb') as tar_file:
|
||||
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}')
|
||||
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}'
|
||||
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}'
|
||||
f"Failed to extract {uploaded_path} to {sandbox_dest}: {process.stderr}"
|
||||
)
|
||||
|
||||
# Delete the local archive
|
||||
|
||||
2
third_party/runtime/impl/modal/__init__.py
vendored
2
third_party/runtime/impl/modal/__init__.py
vendored
@@ -3,4 +3,4 @@
|
||||
This runtime reads configuration directly from environment variables:
|
||||
- MODAL_TOKEN_ID: Modal API token ID for authentication
|
||||
- MODAL_TOKEN_SECRET: Modal API token secret for authentication
|
||||
"""
|
||||
"""
|
||||
|
||||
76
third_party/runtime/impl/modal/modal_runtime.py
vendored
76
third_party/runtime/impl/modal/modal_runtime.py
vendored
@@ -40,7 +40,7 @@ class ModalRuntime(ActionExecutionClient):
|
||||
env_vars (dict[str, str] | None, optional): Environment variables to set. Defaults to None.
|
||||
"""
|
||||
|
||||
container_name_prefix = 'openhands-sandbox-'
|
||||
container_name_prefix = "openhands-sandbox-"
|
||||
sandbox: modal.Sandbox | None
|
||||
sid: str
|
||||
|
||||
@@ -48,7 +48,7 @@ class ModalRuntime(ActionExecutionClient):
|
||||
self,
|
||||
config: OpenHandsConfig,
|
||||
event_stream: EventStream,
|
||||
sid: str = 'default',
|
||||
sid: str = "default",
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
@@ -58,13 +58,17 @@ class ModalRuntime(ActionExecutionClient):
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
|
||||
):
|
||||
# Read Modal API credentials from environment variables
|
||||
modal_token_id = os.getenv('MODAL_TOKEN_ID')
|
||||
modal_token_secret = os.getenv('MODAL_TOKEN_SECRET')
|
||||
modal_token_id = os.getenv("MODAL_TOKEN_ID")
|
||||
modal_token_secret = os.getenv("MODAL_TOKEN_SECRET")
|
||||
|
||||
if not modal_token_id:
|
||||
raise ValueError('MODAL_TOKEN_ID environment variable is required for Modal runtime')
|
||||
raise ValueError(
|
||||
"MODAL_TOKEN_ID environment variable is required for Modal runtime"
|
||||
)
|
||||
if not modal_token_secret:
|
||||
raise ValueError('MODAL_TOKEN_SECRET environment variable is required for Modal runtime')
|
||||
raise ValueError(
|
||||
"MODAL_TOKEN_SECRET environment variable is required for Modal runtime"
|
||||
)
|
||||
|
||||
self.config = config
|
||||
self.sandbox = None
|
||||
@@ -75,14 +79,14 @@ class ModalRuntime(ActionExecutionClient):
|
||||
modal_token_secret,
|
||||
)
|
||||
self.app = modal.App.lookup(
|
||||
'openhands', create_if_missing=True, client=self.modal_client
|
||||
"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.',
|
||||
"warning",
|
||||
"Setting workspace_base is not supported in the modal runtime.",
|
||||
)
|
||||
|
||||
# This value is arbitrary as it's private to the container
|
||||
@@ -96,8 +100,8 @@ class ModalRuntime(ActionExecutionClient):
|
||||
|
||||
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}',
|
||||
"debug",
|
||||
f"Installing extra user-provided dependencies in the runtime image: {self.config.sandbox.runtime_extra_deps}",
|
||||
)
|
||||
|
||||
super().__init__(
|
||||
@@ -116,7 +120,7 @@ class ModalRuntime(ActionExecutionClient):
|
||||
async def connect(self):
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
self.log('debug', f'ModalRuntime `{self.sid}`')
|
||||
self.log("debug", f"ModalRuntime `{self.sid}`")
|
||||
|
||||
self.image = self._get_image_definition(
|
||||
self.base_container_image_id,
|
||||
@@ -127,7 +131,7 @@ class ModalRuntime(ActionExecutionClient):
|
||||
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.log("debug", f"Attaching to existing Modal sandbox: {sandbox_id}")
|
||||
self.sandbox = modal.Sandbox.from_id(
|
||||
sandbox_id, client=self.modal_client
|
||||
)
|
||||
@@ -142,13 +146,13 @@ class ModalRuntime(ActionExecutionClient):
|
||||
self.set_runtime_status(RuntimeStatus.RUNTIME_STARTED)
|
||||
|
||||
if self.sandbox is None:
|
||||
raise Exception('Sandbox not initialized')
|
||||
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}')
|
||||
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.log("debug", "Waiting for client to become ready...")
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
|
||||
self._wait_until_alive()
|
||||
@@ -190,15 +194,15 @@ class ModalRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
base_runtime_image = modal.Image.from_dockerfile(
|
||||
path=os.path.join(build_folder, 'Dockerfile'),
|
||||
path=os.path.join(build_folder, "Dockerfile"),
|
||||
context_mount=modal.Mount.from_local_dir(
|
||||
local_path=build_folder,
|
||||
remote_path='.', # to current WORKDIR
|
||||
remote_path=".", # to current WORKDIR
|
||||
),
|
||||
)
|
||||
else:
|
||||
raise ValueError(
|
||||
'Neither runtime container image nor base container image is set'
|
||||
"Neither runtime container image nor base container image is set"
|
||||
)
|
||||
|
||||
return base_runtime_image.run_commands(
|
||||
@@ -220,29 +224,29 @@ echo 'export INPUTRC=/etc/inputrc' >> /etc/bash.bashrc
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
):
|
||||
try:
|
||||
self.log('debug', 'Preparing to start container...')
|
||||
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),
|
||||
"port": str(self.container_port),
|
||||
"PYTHONUNBUFFERED": "1",
|
||||
"VSCODE_PORT": str(self._vscode_port),
|
||||
}
|
||||
if self.config.debug:
|
||||
environment['DEBUG'] = 'true'
|
||||
environment["DEBUG"] = "true"
|
||||
|
||||
env_secret = modal.Secret.from_dict(environment)
|
||||
|
||||
self.log('debug', f'Sandbox workspace: {sandbox_workspace_dir}')
|
||||
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.log("debug", f"Starting container with command: {sandbox_start_cmd}")
|
||||
self.sandbox = modal.Sandbox.create(
|
||||
*sandbox_start_cmd,
|
||||
secrets=[env_secret],
|
||||
workdir='/openhands/code',
|
||||
workdir="/openhands/code",
|
||||
encrypted_ports=[self.container_port, self._vscode_port],
|
||||
image=self.image,
|
||||
app=self.app,
|
||||
@@ -250,13 +254,13 @@ echo 'export INPUTRC=/etc/inputrc' >> /etc/bash.bashrc
|
||||
timeout=60 * 60,
|
||||
)
|
||||
MODAL_RUNTIME_IDS[self.sid] = self.sandbox.object_id
|
||||
self.log('debug', 'Container started')
|
||||
self.log("debug", "Container started")
|
||||
|
||||
except Exception as e:
|
||||
self.log(
|
||||
'error', f'Error: Instance {self.sid} FAILED to start container!\n'
|
||||
"error", f"Error: Instance {self.sid} FAILED to start container!\n"
|
||||
)
|
||||
self.log('error', str(e))
|
||||
self.log("error", str(e))
|
||||
self.close()
|
||||
raise e
|
||||
|
||||
@@ -270,26 +274,26 @@ echo 'export INPUTRC=/etc/inputrc' >> /etc/bash.bashrc
|
||||
@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}')
|
||||
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')
|
||||
self.log("error", "VSCode token not found")
|
||||
return None
|
||||
if not self.sandbox:
|
||||
self.log('error', 'Sandbox not initialized')
|
||||
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}'
|
||||
+ f"/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}"
|
||||
)
|
||||
|
||||
self.log(
|
||||
'debug',
|
||||
f'VSCode URL: {self._vscode_url}',
|
||||
"debug",
|
||||
f"VSCode URL: {self._vscode_url}",
|
||||
)
|
||||
|
||||
return self._vscode_url
|
||||
|
||||
2
third_party/runtime/impl/runloop/__init__.py
vendored
2
third_party/runtime/impl/runloop/__init__.py
vendored
@@ -2,4 +2,4 @@
|
||||
|
||||
This runtime reads configuration directly from environment variables:
|
||||
- RUNLOOP_API_KEY: API key for Runloop authentication
|
||||
"""
|
||||
"""
|
||||
|
||||
@@ -19,7 +19,7 @@ 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-'
|
||||
CONTAINER_NAME_PREFIX = "openhands-runtime-"
|
||||
|
||||
|
||||
class RunloopRuntime(ActionExecutionClient):
|
||||
@@ -32,7 +32,7 @@ class RunloopRuntime(ActionExecutionClient):
|
||||
self,
|
||||
config: OpenHandsConfig,
|
||||
event_stream: EventStream,
|
||||
sid: str = 'default',
|
||||
sid: str = "default",
|
||||
plugins: list[PluginRequirement] | None = None,
|
||||
env_vars: dict[str, str] | None = None,
|
||||
status_callback: Callable | None = None,
|
||||
@@ -42,10 +42,12 @@ class RunloopRuntime(ActionExecutionClient):
|
||||
git_provider_tokens: PROVIDER_TOKEN_TYPE | None = None,
|
||||
):
|
||||
# Read Runloop API key from environment variable
|
||||
runloop_api_key = os.getenv('RUNLOOP_API_KEY')
|
||||
runloop_api_key = os.getenv("RUNLOOP_API_KEY")
|
||||
if not runloop_api_key:
|
||||
raise ValueError('RUNLOOP_API_KEY environment variable is required for Runloop runtime')
|
||||
|
||||
raise ValueError(
|
||||
"RUNLOOP_API_KEY environment variable is required for Runloop runtime"
|
||||
)
|
||||
|
||||
self.devbox: DevboxView | None = None
|
||||
self.config = config
|
||||
self.runloop_api_client = Runloop(
|
||||
@@ -77,15 +79,15 @@ class RunloopRuntime(ActionExecutionClient):
|
||||
)
|
||||
def _wait_for_devbox(self, devbox: DevboxView) -> DevboxView:
|
||||
"""Pull devbox status until it is running"""
|
||||
if devbox == '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')
|
||||
if devbox.status != "running":
|
||||
raise ConnectionRefusedError("Devbox is not running")
|
||||
|
||||
# Devbox is connected and running
|
||||
logging.debug(f'devbox.id={devbox.id} is running')
|
||||
logging.debug(f"devbox.id={devbox.id} is running")
|
||||
return devbox
|
||||
|
||||
def _create_new_devbox(self) -> DevboxView:
|
||||
@@ -101,26 +103,26 @@ class RunloopRuntime(ActionExecutionClient):
|
||||
# (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)
|
||||
"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',
|
||||
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',
|
||||
resource_size_request="LARGE",
|
||||
launch_commands=[
|
||||
f'mkdir -p {self.config.workspace_mount_path_in_sandbox}'
|
||||
f"mkdir -p {self.config.workspace_mount_path_in_sandbox}"
|
||||
],
|
||||
),
|
||||
metadata={'container-name': self.container_name},
|
||||
metadata={"container-name": self.container_name},
|
||||
)
|
||||
return self._wait_for_devbox(devbox)
|
||||
|
||||
@@ -129,7 +131,7 @@ class RunloopRuntime(ActionExecutionClient):
|
||||
|
||||
if self.attach_to_existing:
|
||||
active_devboxes = self.runloop_api_client.devboxes.list(
|
||||
status='running'
|
||||
status="running"
|
||||
).devboxes
|
||||
self.devbox = next(
|
||||
(devbox for devbox in active_devboxes if devbox.name == self.sid), None
|
||||
@@ -145,11 +147,11 @@ class RunloopRuntime(ActionExecutionClient):
|
||||
)
|
||||
|
||||
self.api_url = tunnel.url
|
||||
logger.info(f'Container started. Server url: {self.api_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...')
|
||||
logger.info("Waiting for client to become ready...")
|
||||
self.set_runtime_status(RuntimeStatus.STARTING_RUNTIME)
|
||||
self._wait_until_alive()
|
||||
|
||||
@@ -157,7 +159,7 @@ class RunloopRuntime(ActionExecutionClient):
|
||||
self.setup_initial_env()
|
||||
|
||||
logger.info(
|
||||
f'Container initialized with plugins: {[plugin.name for plugin in self.plugins]}'
|
||||
f"Container initialized with plugins: {[plugin.name for plugin in self.plugins]}"
|
||||
)
|
||||
self.set_runtime_status(RuntimeStatus.READY)
|
||||
|
||||
@@ -192,12 +194,12 @@ class RunloopRuntime(ActionExecutionClient):
|
||||
id=self.devbox.id,
|
||||
port=self._vscode_port,
|
||||
).url
|
||||
+ f'/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}'
|
||||
+ f"/?tkn={token}&folder={self.config.workspace_mount_path_in_sandbox}"
|
||||
)
|
||||
|
||||
self.log(
|
||||
'debug',
|
||||
f'VSCode URL: {self._vscode_url}',
|
||||
"debug",
|
||||
f"VSCode URL: {self._vscode_url}",
|
||||
)
|
||||
|
||||
return self._vscode_url
|
||||
|
||||
Reference in New Issue
Block a user