chore(lint): Apply comprehensive linting and formatting fixes (#10287)

Co-authored-by: openhands <openhands@all-hands.dev>
This commit is contained in:
Xingyao Wang
2025-08-13 15:13:19 -04:00
committed by GitHub
parent e39bf80239
commit c2f46200c0
164 changed files with 526 additions and 1023 deletions

View File

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

View File

@@ -1 +1 @@
"""Third-party runtime implementations."""
"""Third-party runtime implementations."""

View File

@@ -1 +1 @@
"""Third-party runtime implementation modules."""
"""Third-party runtime implementation modules."""

View File

@@ -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')
"""
"""

View File

@@ -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)}."

View File

@@ -2,4 +2,4 @@
This runtime reads configuration directly from environment variables:
- E2B_API_KEY: API key for E2B authentication
"""
"""

View File

@@ -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}")

View File

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

View File

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

View File

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

View File

@@ -2,4 +2,4 @@
This runtime reads configuration directly from environment variables:
- RUNLOOP_API_KEY: API key for Runloop authentication
"""
"""

View File

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