Merge PR #451: feat: Add Daytona environment backend
Authored by rovle. Adds Daytona as the sixth terminal execution backend with cloud sandboxes, persistent workspaces, and full CLI/gateway integration. Includes 24 unit tests and 8 integration tests.
This commit is contained in:
commit
39299e2de4
22 changed files with 865 additions and 30 deletions
|
|
@ -71,7 +71,8 @@ DEFAULT_CONFIG = {
|
|||
"docker_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
||||
"singularity_image": "docker://nikolaik/python-nodejs:python3.11-nodejs20",
|
||||
"modal_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
||||
# Container resource limits (docker, singularity, modal — ignored for local/ssh)
|
||||
"daytona_image": "nikolaik/python-nodejs:python3.11-nodejs20",
|
||||
# Container resource limits (docker, singularity, modal, daytona — ignored for local/ssh)
|
||||
"container_cpu": 1,
|
||||
"container_memory": 5120, # MB (default 5GB)
|
||||
"container_disk": 51200, # MB (default 50GB)
|
||||
|
|
@ -761,6 +762,10 @@ def show_config():
|
|||
print(f" Modal image: {terminal.get('modal_image', 'python:3.11')}")
|
||||
modal_token = get_env_value('MODAL_TOKEN_ID')
|
||||
print(f" Modal token: {'configured' if modal_token else '(not set)'}")
|
||||
elif terminal.get('backend') == 'daytona':
|
||||
print(f" Daytona image: {terminal.get('daytona_image', 'nikolaik/python-nodejs:python3.11-nodejs20')}")
|
||||
daytona_key = get_env_value('DAYTONA_API_KEY')
|
||||
print(f" API key: {'configured' if daytona_key else '(not set)'}")
|
||||
elif terminal.get('backend') == 'ssh':
|
||||
ssh_host = get_env_value('TERMINAL_SSH_HOST')
|
||||
ssh_user = get_env_value('TERMINAL_SSH_USER')
|
||||
|
|
@ -886,6 +891,7 @@ def set_config_value(key: str, value: str):
|
|||
"terminal.docker_image": "TERMINAL_DOCKER_IMAGE",
|
||||
"terminal.singularity_image": "TERMINAL_SINGULARITY_IMAGE",
|
||||
"terminal.modal_image": "TERMINAL_MODAL_IMAGE",
|
||||
"terminal.daytona_image": "TERMINAL_DAYTONA_IMAGE",
|
||||
"terminal.cwd": "TERMINAL_CWD",
|
||||
"terminal.timeout": "TERMINAL_TIMEOUT",
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'}")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue