mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 01:21:43 +00:00
feat(koyeb): add Koyeb sandboxes as a backend for terminal execution
This commit is contained in:
parent
5dda4cab41
commit
57e33cf284
5 changed files with 566 additions and 14 deletions
|
|
@ -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')}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue