feat(koyeb): add Koyeb sandboxes as a backend for terminal execution

This commit is contained in:
Vibe Nuage Agent 2026-04-22 15:53:20 +00:00 committed by Fabzerito
parent 5dda4cab41
commit 57e33cf284
5 changed files with 566 additions and 14 deletions

View file

@ -2,16 +2,17 @@
"""
Terminal Tool Module
A terminal tool that executes commands in local, Docker, Modal, SSH, Singularity, and Daytona environments.
Supports local execution, containerized backends, and Modal cloud sandboxes, including managed gateway mode.
A terminal tool that executes commands in local, Docker, Modal, SSH, Singularity, Daytona, and Koyeb environments.
Supports local execution, containerized backends, and cloud sandboxes (Modal, Daytona, Koyeb), including managed gateway mode.
Environment Selection (via TERMINAL_ENV environment variable):
- "local": Execute directly on the host machine (default, fastest)
- "docker": Execute in Docker containers (isolated, requires Docker)
- "modal": Execute in Modal cloud sandboxes (direct Modal or managed gateway)
- "koyeb": Execute in Koyeb cloud sandboxes
Features:
- Multiple execution backends (local, docker, modal)
- Multiple execution backends (local, docker, modal, koyeb)
- Background task support
- VM/container lifecycle management
- Automatic cleanup after inactivity
@ -855,7 +856,7 @@ def _get_env_config() -> Dict[str, Any]:
):
host_cwd = candidate
cwd = "/workspace"
elif env_type in ("modal", "docker", "singularity", "daytona") and cwd:
elif env_type in ("modal", "docker", "singularity", "daytona", "koyeb") and cwd:
# Host paths and relative paths that won't work inside containers
is_host_path = any(cwd.startswith(p) for p in host_prefixes)
is_relative = not os.path.isabs(cwd) # e.g. "." or "src/"
@ -873,6 +874,7 @@ def _get_env_config() -> Dict[str, Any]:
"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),
"koyeb_image": os.getenv("TERMINAL_KOYEB_IMAGE", default_image),
"cwd": cwd,
"host_cwd": host_cwd,
"docker_mount_cwd_to_workspace": mount_docker_cwd,
@ -891,12 +893,15 @@ def _get_env_config() -> Dict[str, Any]:
os.getenv("TERMINAL_PERSISTENT_SHELL", "true"),
).lower() in ("true", "1", "yes"),
"local_persistent": os.getenv("TERMINAL_LOCAL_PERSISTENT", "false").lower() in ("true", "1", "yes"),
# Container resource config (applies to docker, singularity, modal, daytona -- ignored for local/ssh)
# Container resource config (applies to docker, singularity, modal, daytona, koyeb -- ignored for local/ssh)
"container_cpu": _parse_env_var("TERMINAL_CONTAINER_CPU", "1", float, "number"),
"container_memory": _parse_env_var("TERMINAL_CONTAINER_MEMORY", "5120"), # MB (default 5GB)
"container_disk": _parse_env_var("TERMINAL_CONTAINER_DISK", "51200"), # MB (default 50GB)
"container_persistent": os.getenv("TERMINAL_CONTAINER_PERSISTENT", "true").lower() in ("true", "1", "yes"),
"docker_volumes": _parse_env_var("TERMINAL_DOCKER_VOLUMES", "[]", json.loads, "valid JSON"),
# Koyeb-specific config
"koyeb_instance_type": os.getenv("TERMINAL_KOYEB_INSTANCE_TYPE", "micro"),
"koyeb_region": os.getenv("KOYEB_REGION", "na"),
}
@ -918,8 +923,8 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
Create an execution environment for sandboxed command execution.
Args:
env_type: One of "local", "docker", "singularity", "modal", "daytona", "ssh"
image: Docker/Singularity/Modal image name (ignored for local/ssh)
env_type: One of "local", "docker", "singularity", "modal", "daytona", "koyeb", "ssh"
image: Docker/Singularity/Modal/Koyeb image name (ignored for local/ssh)
cwd: Working directory
timeout: Default command timeout
ssh_config: SSH connection config (for env_type="ssh")
@ -1022,6 +1027,22 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
persistent_filesystem=persistent, task_id=task_id,
)
elif env_type == "koyeb":
# Lazy import so koyeb SDK is only required when backend is selected.
from tools.environments.koyeb import KoyebEnvironment as _KoyebEnvironment
# Get Koyeb-specific configuration
koyeb_instance_type = cc.get("koyeb_instance_type", "micro")
koyeb_region = cc.get("koyeb_region")
return _KoyebEnvironment(
image=image, cwd=cwd, timeout=timeout,
cpu=int(cpu), memory=memory, disk=disk,
persistent_filesystem=persistent, task_id=task_id,
instance_type=koyeb_instance_type,
region=koyeb_region,
)
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")
@ -1035,7 +1056,7 @@ def _create_environment(env_type: str, image: str, cwd: str, timeout: int,
)
else:
raise ValueError(f"Unknown environment type: {env_type}. Use 'local', 'docker', 'singularity', 'modal', 'daytona', or 'ssh'")
raise ValueError(f"Unknown environment type: {env_type}. Use 'local', 'docker', 'singularity', 'modal', 'daytona', 'koyeb', or 'ssh'")
def _cleanup_inactive_envs(lifetime_seconds: int = 300):
@ -1462,6 +1483,8 @@ def terminal_tool(
image = overrides.get("modal_image") or config["modal_image"]
elif env_type == "daytona":
image = overrides.get("daytona_image") or config["daytona_image"]
elif env_type == "koyeb":
image = overrides.get("koyeb_image") or config["koyeb_image"]
else:
image = ""
@ -1538,7 +1561,7 @@ def terminal_tool(
}
container_config = None
if env_type in ("docker", "singularity", "modal", "daytona"):
if env_type in ("docker", "singularity", "modal", "daytona", "koyeb"):
container_config = {
"container_cpu": config.get("container_cpu", 1),
"container_memory": config.get("container_memory", 5120),
@ -1948,10 +1971,14 @@ def check_terminal_requirements() -> bool:
from daytona import Daytona # noqa: F401 — SDK presence check
return os.getenv("DAYTONA_API_KEY") is not None
elif env_type == "koyeb":
from koyeb import Sandbox # noqa: F401 — SDK presence check
return os.getenv("KOYEB_API_TOKEN") is not None
else:
logger.error(
"Unknown TERMINAL_ENV '%s'. Use one of: local, docker, singularity, "
"modal, daytona, ssh.",
"modal, daytona, koyeb, ssh.",
env_type,
)
return False
@ -1970,6 +1997,7 @@ if __name__ == "__main__":
print(f" Environment type: {config['env_type']}")
print(f" Docker image: {config['docker_image']}")
print(f" Modal image: {config['modal_image']}")
print(f" Koyeb image: {config['koyeb_image']}")
print(f" Working directory: {config['cwd']}")
print(f" Default timeout: {config['timeout']}s")
print(f" Lifetime: {config['lifetime_seconds']}s")
@ -1991,11 +2019,12 @@ 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/daytona/ssh)")
print(f" TERMINAL_ENV: {os.getenv('TERMINAL_ENV', 'local')} (local/docker/singularity/modal/daytona/koyeb/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_KOYEB_IMAGE: {os.getenv('TERMINAL_KOYEB_IMAGE', default_img)}")
print(f" TERMINAL_CWD: {os.getenv('TERMINAL_CWD', os.getcwd())}")
from hermes_constants import display_hermes_home as _dhh
print(f" TERMINAL_SANDBOX_DIR: {os.getenv('TERMINAL_SANDBOX_DIR', f'{_dhh()}/sandboxes')}")