From c43451a50b5cdeaa31ac56a39876523fd0088f84 Mon Sep 17 00:00:00 2001 From: rovle Date: Thu, 5 Mar 2026 00:41:12 -0800 Subject: [PATCH] feat(terminal): integrate Daytona backend into tool pipeline Add Daytona to image selection, container_config guards, environment factory, requirements check, and diagnostics in terminal_tool.py and file_tools.py. Also add to sandboxed-backend approval bypass. Signed-off-by: rovle --- tools/approval.py | 2 +- tools/file_tools.py | 4 +++- tools/terminal_tool.py | 32 ++++++++++++++++++++++++-------- 3 files changed, 28 insertions(+), 10 deletions(-) diff --git a/tools/approval.py b/tools/approval.py index f2356533f..cdf19e443 100644 --- a/tools/approval.py +++ b/tools/approval.py @@ -247,7 +247,7 @@ def check_dangerous_command(command: str, env_type: str, Returns: {"approved": True/False, "message": str or None, ...} """ - if env_type in ("docker", "singularity", "modal"): + if env_type in ("docker", "singularity", "modal", "daytona"): return {"approved": True, "message": None} is_dangerous, pattern_key, description = detect_dangerous_command(command) diff --git a/tools/file_tools.py b/tools/file_tools.py index a864f4392..b29d2d274 100644 --- a/tools/file_tools.py +++ b/tools/file_tools.py @@ -75,6 +75,8 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations: image = overrides.get("singularity_image") or config["singularity_image"] elif env_type == "modal": image = overrides.get("modal_image") or config["modal_image"] + elif env_type == "daytona": + image = overrides.get("daytona_image") or config["daytona_image"] else: image = "" @@ -82,7 +84,7 @@ def _get_file_ops(task_id: str = "default") -> ShellFileOperations: logger.info("Creating new %s environment for task %s...", env_type, task_id[:8]) container_config = None - if env_type in ("docker", "singularity", "modal"): + if env_type in ("docker", "singularity", "modal", "daytona"): container_config = { "container_cpu": config.get("container_cpu", 1), "container_memory": config.get("container_memory", 5120), diff --git a/tools/terminal_tool.py b/tools/terminal_tool.py index 096ac207f..e123262c5 100644 --- a/tools/terminal_tool.py +++ b/tools/terminal_tool.py @@ -423,7 +423,7 @@ def _get_env_config() -> Dict[str, Any]: # catches the case where cli.py (or .env) leaked the host's CWD. # SSH is excluded since /home/ paths are valid on remote machines. cwd = os.getenv("TERMINAL_CWD", default_cwd) - if env_type in ("modal", "docker", "singularity") and cwd: + if env_type in ("modal", "docker", "singularity", "daytona") and cwd: host_prefixes = ("/Users/", "C:\\", "C:/") if any(cwd.startswith(p) for p in host_prefixes) and cwd != default_cwd: logger.info("Ignoring TERMINAL_CWD=%r for %s backend " @@ -436,6 +436,7 @@ def _get_env_config() -> Dict[str, Any]: "docker_image": os.getenv("TERMINAL_DOCKER_IMAGE", default_image), "singularity_image": os.getenv("TERMINAL_SINGULARITY_IMAGE", f"docker://{default_image}"), "modal_image": os.getenv("TERMINAL_MODAL_IMAGE", default_image), + "daytona_image": os.getenv("TERMINAL_DAYTONA_IMAGE", default_image), "cwd": cwd, "timeout": int(os.getenv("TERMINAL_TIMEOUT", "180")), "lifetime_seconds": int(os.getenv("TERMINAL_LIFETIME_SECONDS", "300")), @@ -444,7 +445,7 @@ def _get_env_config() -> Dict[str, Any]: "ssh_user": os.getenv("TERMINAL_SSH_USER", ""), "ssh_port": int(os.getenv("TERMINAL_SSH_PORT", "22")), "ssh_key": os.getenv("TERMINAL_SSH_KEY", ""), - # Container resource config (applies to docker, singularity, modal -- ignored for local/ssh) + # Container resource config (applies to docker, singularity, modal, daytona -- ignored for local/ssh) "container_cpu": float(os.getenv("TERMINAL_CONTAINER_CPU", "1")), "container_memory": int(os.getenv("TERMINAL_CONTAINER_MEMORY", "5120")), # MB (default 5GB) "container_disk": int(os.getenv("TERMINAL_CONTAINER_DISK", "51200")), # MB (default 50GB) @@ -460,7 +461,7 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int, Create an execution environment from mini-swe-agent. Args: - env_type: One of "local", "docker", "singularity", "modal", "ssh" + env_type: One of "local", "docker", "singularity", "modal", "daytona", "ssh" image: Docker/Singularity/Modal image name (ignored for local/ssh) cwd: Working directory timeout: Default command timeout @@ -511,6 +512,15 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int, persistent_filesystem=persistent, task_id=task_id, ) + elif env_type == "daytona": + # Lazy import so daytona SDK is only required when backend is selected. + from tools.environments.daytona import DaytonaEnvironment as _DaytonaEnvironment + return _DaytonaEnvironment( + image=image, cwd=cwd, timeout=timeout, + cpu=int(cpu), memory=memory, disk=disk, + persistent_filesystem=persistent, task_id=task_id, + ) + elif env_type == "ssh": if not ssh_config or not ssh_config.get("host") or not ssh_config.get("user"): raise ValueError("SSH environment requires ssh_host and ssh_user to be configured") @@ -522,9 +532,9 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int, cwd=cwd, timeout=timeout, ) - + else: - raise ValueError(f"Unknown environment type: {env_type}. Use 'local', 'docker', 'singularity', 'modal', or 'ssh'") + raise ValueError(f"Unknown environment type: {env_type}. Use 'local', 'docker', 'singularity', 'modal', 'daytona', or 'ssh'") def _cleanup_inactive_envs(lifetime_seconds: int = 300): @@ -799,9 +809,11 @@ def terminal_tool( image = overrides.get("singularity_image") or config["singularity_image"] elif env_type == "modal": image = overrides.get("modal_image") or config["modal_image"] + elif env_type == "daytona": + image = overrides.get("daytona_image") or config["daytona_image"] else: image = "" - + cwd = overrides.get("cwd") or config["cwd"] default_timeout = config["timeout"] effective_timeout = timeout or default_timeout @@ -851,7 +863,7 @@ def terminal_tool( } container_config = None - if env_type in ("docker", "singularity", "modal"): + if env_type in ("docker", "singularity", "modal", "daytona"): container_config = { "container_cpu": config.get("container_cpu", 1), "container_memory": config.get("container_memory", 5120), @@ -1090,6 +1102,9 @@ def check_terminal_requirements() -> bool: from minisweagent.environments.extra.swerex_modal import SwerexModalEnvironment # Check for modal token return os.getenv("MODAL_TOKEN_ID") is not None or Path.home().joinpath(".modal.toml").exists() + elif env_type == "daytona": + from daytona import Daytona + return os.getenv("DAYTONA_API_KEY") is not None else: return False except Exception as e: @@ -1128,10 +1143,11 @@ if __name__ == "__main__": print("\nEnvironment Variables:") default_img = "nikolaik/python-nodejs:python3.11-nodejs20" - print(f" TERMINAL_ENV: {os.getenv('TERMINAL_ENV', 'local')} (local/docker/singularity/modal/ssh)") + print(f" TERMINAL_ENV: {os.getenv('TERMINAL_ENV', 'local')} (local/docker/singularity/modal/daytona/ssh)") print(f" TERMINAL_DOCKER_IMAGE: {os.getenv('TERMINAL_DOCKER_IMAGE', default_img)}") print(f" TERMINAL_SINGULARITY_IMAGE: {os.getenv('TERMINAL_SINGULARITY_IMAGE', f'docker://{default_img}')}") print(f" TERMINAL_MODAL_IMAGE: {os.getenv('TERMINAL_MODAL_IMAGE', default_img)}") + print(f" TERMINAL_DAYTONA_IMAGE: {os.getenv('TERMINAL_DAYTONA_IMAGE', default_img)}") print(f" TERMINAL_CWD: {os.getenv('TERMINAL_CWD', os.getcwd())}") print(f" TERMINAL_SANDBOX_DIR: {os.getenv('TERMINAL_SANDBOX_DIR', '~/.hermes/sandboxes')}") print(f" TERMINAL_TIMEOUT: {os.getenv('TERMINAL_TIMEOUT', '60')}")