diff --git a/cli-config.yaml.example b/cli-config.yaml.example index 0ca406358..a61531ac9 100644 --- a/cli-config.yaml.example +++ b/cli-config.yaml.example @@ -116,8 +116,22 @@ terminal: # timeout: 180 # lifetime_seconds: 300 # modal_image: "nikolaik/python-nodejs:python3.11-nodejs20" + +# ----------------------------------------------------------------------------- +# OPTION 6: Daytona cloud execution +# Commands run in Daytona cloud sandboxes +# Great for: Cloud dev environments, persistent workspaces, team collaboration +# Requires: pip install daytona, DAYTONA_API_KEY env var +# ----------------------------------------------------------------------------- +# terminal: +# backend: "daytona" +# cwd: "/home/daytona" +# timeout: 180 +# lifetime_seconds: 300 +# daytona_image: "nikolaik/python-nodejs:python3.11-nodejs20" + # -# --- Container resource limits (docker, singularity, modal -- ignored for local/ssh) --- +# --- Container resource limits (docker, singularity, modal, daytona -- ignored for local/ssh) --- # These settings apply to all container backends. They control the resources # allocated to the sandbox and whether its filesystem persists across sessions. container_cpu: 1 # CPU cores diff --git a/hermes_cli/doctor.py b/hermes_cli/doctor.py index 031c6eaf8..36795c016 100644 --- a/hermes_cli/doctor.py +++ b/hermes_cli/doctor.py @@ -355,6 +355,21 @@ def run_doctor(args): check_fail("TERMINAL_SSH_HOST not set", "(required for TERMINAL_ENV=ssh)") issues.append("Set TERMINAL_SSH_HOST in .env") + # Daytona (if using daytona backend) + if terminal_env == "daytona": + daytona_key = os.getenv("DAYTONA_API_KEY") + if daytona_key: + check_ok("Daytona API key", "(configured)") + else: + check_fail("DAYTONA_API_KEY not set", "(required for TERMINAL_ENV=daytona)") + issues.append("Set DAYTONA_API_KEY environment variable") + try: + from daytona import Daytona + check_ok("daytona SDK", "(installed)") + except ImportError: + check_fail("daytona SDK not installed", "(pip install daytona)") + issues.append("Install daytona SDK: pip install daytona") + # Node.js + agent-browser (for browser automation tools) if shutil.which("node"): check_ok("Node.js") diff --git a/hermes_cli/setup.py b/hermes_cli/setup.py index 8a1dc78d1..a312a20fd 100644 --- a/hermes_cli/setup.py +++ b/hermes_cli/setup.py @@ -980,19 +980,20 @@ def run_setup_wizard(args): terminal_choices.extend([ "Modal (cloud execution, GPU access, serverless)", + "Daytona (cloud sandboxes, persistent workspaces)", "SSH (run commands on a remote server)", f"Keep current ({current_backend})" ]) # Build index map based on available choices if is_linux: - backend_to_idx = {'local': 0, 'docker': 1, 'singularity': 2, 'modal': 3, 'ssh': 4} - idx_to_backend = {0: 'local', 1: 'docker', 2: 'singularity', 3: 'modal', 4: 'ssh'} - keep_current_idx = 5 + backend_to_idx = {'local': 0, 'docker': 1, 'singularity': 2, 'modal': 3, 'daytona': 4, 'ssh': 5} + idx_to_backend = {0: 'local', 1: 'docker', 2: 'singularity', 3: 'modal', 4: 'daytona', 5: 'ssh'} + keep_current_idx = 6 else: - backend_to_idx = {'local': 0, 'docker': 1, 'modal': 2, 'ssh': 3} - idx_to_backend = {0: 'local', 1: 'docker', 2: 'modal', 3: 'ssh'} - keep_current_idx = 4 + backend_to_idx = {'local': 0, 'docker': 1, 'modal': 2, 'daytona': 3, 'ssh': 4} + idx_to_backend = {0: 'local', 1: 'docker', 2: 'modal', 3: 'daytona', 4: 'ssh'} + keep_current_idx = 5 if current_backend == 'singularity': print_warning("Singularity is only available on Linux - please select a different backend") @@ -1067,7 +1068,7 @@ def run_setup_wizard(args): print() print_info("Note: Container resource settings (CPU, memory, disk, persistence)") - print_info("are in your config but only apply to Docker/Singularity/Modal backends.") + print_info("are in your config but only apply to Docker/Singularity/Modal/Daytona backends.") if prompt_yes_no(" Enable sudo support? (allows agent to run sudo commands)", False): print_warning(" SECURITY WARNING: Sudo password will be stored in plaintext") @@ -1151,7 +1152,52 @@ def run_setup_wizard(args): _prompt_container_resources(config) print_success("Terminal set to Modal") - + + elif selected_backend == 'daytona': + config.setdefault('terminal', {})['backend'] = 'daytona' + default_daytona = config.get('terminal', {}).get('daytona_image', 'nikolaik/python-nodejs:python3.11-nodejs20') + print_info("Daytona Cloud Configuration:") + print_info("Get your API key at: https://app.daytona.io/dashboard/keys") + + # Check if daytona SDK is installed + try: + from daytona import Daytona + print_info("daytona SDK: installed ✓") + except ImportError: + print_info("Installing required package: daytona...") + import subprocess + import shutil + uv_bin = shutil.which("uv") + if uv_bin: + result = subprocess.run( + [uv_bin, "pip", "install", "daytona"], + capture_output=True, text=True + ) + else: + result = subprocess.run( + [sys.executable, "-m", "pip", "install", "daytona"], + capture_output=True, text=True + ) + if result.returncode == 0: + print_success("daytona SDK installed") + else: + print_warning("Failed to install daytona SDK — install manually:") + print_info(' pip install daytona') + + daytona_image = prompt(" Container image", default_daytona) + config['terminal']['daytona_image'] = daytona_image + + current_key = get_env_value('DAYTONA_API_KEY') + if current_key: + print_info(f" API Key: {current_key[:8]}... (configured)") + + api_key = prompt(" Daytona API key", current_key or "", password=True) + if api_key: + save_env_value("DAYTONA_API_KEY", api_key) + + _prompt_container_resources(config) + print_success("Terminal set to Daytona") + elif selected_backend == 'ssh': config.setdefault('terminal', {})['backend'] = 'ssh' print_info("SSH Remote Execution Configuration:") @@ -1181,7 +1227,7 @@ def run_setup_wizard(args): print() print_info("Note: Container resource settings (CPU, memory, disk, persistence)") - print_info("are in your config but only apply to Docker/Singularity/Modal backends.") + print_info("are in your config but only apply to Docker/Singularity/Modal/Daytona backends.") print_success("Terminal set to SSH") # else: Keep current (selected_backend is None) @@ -1192,6 +1238,9 @@ def run_setup_wizard(args): docker_image = config.get('terminal', {}).get('docker_image') if docker_image: save_env_value("TERMINAL_DOCKER_IMAGE", docker_image) + daytona_image = config.get('terminal', {}).get('daytona_image') + if daytona_image: + save_env_value("TERMINAL_DAYTONA_IMAGE", daytona_image) # ========================================================================= # Step 5: Agent Settings diff --git a/hermes_cli/status.py b/hermes_cli/status.py index f1d3a7edf..48c962def 100644 --- a/hermes_cli/status.py +++ b/hermes_cli/status.py @@ -163,6 +163,9 @@ def show_status(args): elif terminal_env == "docker": docker_image = os.getenv("TERMINAL_DOCKER_IMAGE", "python:3.11-slim") print(f" Docker Image: {docker_image}") + elif terminal_env == "daytona": + daytona_image = os.getenv("TERMINAL_DAYTONA_IMAGE", "nikolaik/python-nodejs:python3.11-nodejs20") + print(f" Daytona Image: {daytona_image}") sudo_password = os.getenv("SUDO_PASSWORD", "") print(f" Sudo: {check_mark(bool(sudo_password))} {'enabled' if sudo_password else 'disabled'}")