suna/backend/sandbox/sandbox.py

213 lines
8.0 KiB
Python
Raw Normal View History

2025-04-09 04:09:45 +08:00
import os
2025-04-23 17:53:38 +08:00
from typing import Optional
2025-04-09 04:09:45 +08:00
2025-04-18 23:58:08 +08:00
from daytona_sdk import Daytona, DaytonaConfig, CreateSandboxParams, Sandbox, SessionExecuteRequest
2025-04-11 00:02:21 +08:00
from daytona_api_client.models.workspace_state import WorkspaceState
2025-04-09 07:37:06 +08:00
from dotenv import load_dotenv
2025-04-09 04:09:45 +08:00
from agentpress.tool import Tool
from utils.logger import logger
2025-04-24 08:45:58 +08:00
from utils.config import config
2025-04-13 05:40:01 +08:00
from utils.files_utils import clean_path
2025-04-23 21:34:18 +08:00
from agentpress.thread_manager import ThreadManager
2025-04-09 04:09:45 +08:00
2025-04-09 07:37:06 +08:00
load_dotenv()
2025-04-11 00:02:21 +08:00
logger.debug("Initializing Daytona sandbox configuration")
2025-04-24 08:45:58 +08:00
daytona_config = DaytonaConfig(
api_key=config.DAYTONA_API_KEY,
server_url=config.DAYTONA_SERVER_URL,
target=config.DAYTONA_TARGET
2025-04-09 04:09:45 +08:00
)
2025-04-10 00:44:17 +08:00
2025-04-24 08:45:58 +08:00
if daytona_config.api_key:
2025-04-10 00:44:17 +08:00
logger.debug("Daytona API key configured successfully")
else:
logger.warning("No Daytona API key found in environment variables")
2025-04-24 08:45:58 +08:00
if daytona_config.server_url:
logger.debug(f"Daytona server URL set to: {daytona_config.server_url}")
2025-04-10 00:44:17 +08:00
else:
logger.warning("No Daytona server URL found in environment variables")
2025-04-24 08:45:58 +08:00
if daytona_config.target:
logger.debug(f"Daytona target set to: {daytona_config.target}")
2025-04-10 00:44:17 +08:00
else:
logger.warning("No Daytona target found in environment variables")
2025-04-24 08:45:58 +08:00
daytona = Daytona(daytona_config)
2025-04-10 00:44:17 +08:00
logger.debug("Daytona client initialized")
2025-04-09 04:09:45 +08:00
async def get_or_start_sandbox(sandbox_id: str):
2025-04-13 05:40:01 +08:00
"""Retrieve a sandbox by ID, check its state, and start it if needed."""
2025-04-11 00:02:21 +08:00
logger.info(f"Getting or starting sandbox with ID: {sandbox_id}")
try:
sandbox = daytona.get_current_sandbox(sandbox_id)
# Check if sandbox needs to be started
if sandbox.instance.state == WorkspaceState.ARCHIVED or sandbox.instance.state == WorkspaceState.STOPPED:
logger.info(f"Sandbox is in {sandbox.instance.state} state. Starting...")
try:
daytona.start(sandbox)
# Wait a moment for the sandbox to initialize
2025-04-13 05:40:01 +08:00
# sleep(5)
2025-04-11 00:02:21 +08:00
# Refresh sandbox state after starting
sandbox = daytona.get_current_sandbox(sandbox_id)
2025-04-18 23:58:08 +08:00
# Start supervisord in a session when restarting
start_supervisord_session(sandbox)
2025-04-11 00:02:21 +08:00
except Exception as e:
logger.error(f"Error starting sandbox: {e}")
raise e
logger.info(f"Sandbox {sandbox_id} is ready")
return sandbox
except Exception as e:
logger.error(f"Error retrieving or starting sandbox: {str(e)}")
raise e
2025-04-09 07:37:06 +08:00
2025-04-18 23:58:08 +08:00
def start_supervisord_session(sandbox: Sandbox):
"""Start supervisord in a session."""
session_id = "supervisord-session"
try:
logger.info(f"Creating session {session_id} for supervisord")
sandbox.process.create_session(session_id)
# Execute supervisord command
sandbox.process.execute_session_command(session_id, SessionExecuteRequest(
command="exec /usr/bin/supervisord -n -c /etc/supervisor/conf.d/supervisord.conf",
var_async=True
))
logger.info(f"Supervisord started in session {session_id}")
except Exception as e:
logger.error(f"Error starting supervisord session: {str(e)}")
raise e
2025-04-24 21:36:50 +08:00
def create_sandbox(password: str, project_id: str = None):
2025-04-11 00:02:21 +08:00
"""Create a new sandbox with all required services configured and running."""
2025-04-23 16:13:07 +08:00
logger.debug("Creating new Daytona sandbox environment")
2025-04-10 00:44:17 +08:00
logger.debug("Configuring sandbox with browser-use image and environment variables")
2025-04-23 16:13:07 +08:00
labels = None
2025-04-24 21:36:50 +08:00
if project_id:
logger.debug(f"Using sandbox_id as label: {project_id}")
labels = {'id': project_id}
2025-04-22 23:53:12 +08:00
2025-04-23 16:13:07 +08:00
params = CreateSandboxParams(
2025-04-22 23:55:16 +08:00
image="adamcohenhillel/kortix-suna:0.0.20",
public=True,
2025-04-23 16:13:07 +08:00
labels=labels,
2025-04-09 04:09:45 +08:00
env_vars={
"CHROME_PERSISTENT_SESSION": "true",
2025-04-22 20:21:48 +08:00
"RESOLUTION": "1024x768x24",
"RESOLUTION_WIDTH": "1024",
"RESOLUTION_HEIGHT": "768",
2025-04-09 04:09:45 +08:00
"VNC_PASSWORD": password,
"ANONYMIZED_TELEMETRY": "false",
"CHROME_PATH": "",
"CHROME_USER_DATA": "",
"CHROME_DEBUGGING_PORT": "9222",
"CHROME_DEBUGGING_HOST": "localhost",
"CHROME_CDP": ""
},
2025-04-23 03:26:12 +08:00
resources={
"cpu": 2,
"memory": 4,
"disk": 5,
}
2025-04-23 16:13:07 +08:00
)
# Create the sandbox
sandbox = daytona.create(params)
logger.debug(f"Sandbox created with ID: {sandbox.id}")
2025-04-10 00:44:17 +08:00
2025-04-18 23:58:08 +08:00
# Start supervisord in a session for new sandbox
start_supervisord_session(sandbox)
2025-04-09 04:09:45 +08:00
2025-04-23 16:13:07 +08:00
logger.debug(f"Sandbox environment successfully initialized")
2025-04-09 04:09:45 +08:00
return sandbox
class SandboxToolsBase(Tool):
2025-04-23 17:53:38 +08:00
"""Base class for all sandbox tools that provides project-based sandbox access."""
2025-04-09 04:09:45 +08:00
2025-04-11 00:02:21 +08:00
# Class variable to track if sandbox URLs have been printed
_urls_printed = False
2025-04-23 17:53:38 +08:00
def __init__(self, project_id: str, thread_manager: Optional[ThreadManager] = None):
2025-04-09 04:09:45 +08:00
super().__init__()
2025-04-23 17:53:38 +08:00
self.project_id = project_id
self.thread_manager = thread_manager
2025-04-09 20:46:13 +08:00
self.workspace_path = "/workspace"
2025-04-23 17:53:38 +08:00
self._sandbox = None
self._sandbox_id = None
self._sandbox_pass = None
2025-04-09 04:09:45 +08:00
2025-04-23 17:53:38 +08:00
async def _ensure_sandbox(self) -> Sandbox:
"""Ensure we have a valid sandbox instance, retrieving it from the project if needed."""
if self._sandbox is None:
try:
# Get database client
client = await self.thread_manager.db.client
# Get project data
project = await client.table('projects').select('*').eq('project_id', self.project_id).execute()
if not project.data or len(project.data) == 0:
raise ValueError(f"Project {self.project_id} not found")
project_data = project.data[0]
sandbox_info = project_data.get('sandbox', {})
if not sandbox_info.get('id'):
raise ValueError(f"No sandbox found for project {self.project_id}")
# Store sandbox info
self._sandbox_id = sandbox_info['id']
self._sandbox_pass = sandbox_info.get('pass')
# Get or start the sandbox
self._sandbox = await get_or_start_sandbox(self._sandbox_id)
2025-04-23 22:56:24 +08:00
# # Log URLs if not already printed
# if not SandboxToolsBase._urls_printed:
# vnc_link = self._sandbox.get_preview_link(6080)
# website_link = self._sandbox.get_preview_link(8080)
2025-04-23 17:53:38 +08:00
2025-04-23 22:56:24 +08:00
# vnc_url = vnc_link.url if hasattr(vnc_link, 'url') else str(vnc_link)
# website_url = website_link.url if hasattr(website_link, 'url') else str(website_link)
2025-04-23 17:53:38 +08:00
2025-04-23 22:56:24 +08:00
# print("\033[95m***")
# print(f"VNC URL: {vnc_url}")
# print(f"Website URL: {website_url}")
# print("***\033[0m")
# SandboxToolsBase._urls_printed = True
2025-04-23 17:53:38 +08:00
except Exception as e:
logger.error(f"Error retrieving sandbox for project {self.project_id}: {str(e)}", exc_info=True)
raise e
2025-04-09 04:09:45 +08:00
2025-04-23 17:53:38 +08:00
return self._sandbox
@property
def sandbox(self) -> Sandbox:
"""Get the sandbox instance, ensuring it exists."""
if self._sandbox is None:
raise RuntimeError("Sandbox not initialized. Call _ensure_sandbox() first.")
return self._sandbox
@property
def sandbox_id(self) -> str:
"""Get the sandbox ID, ensuring it exists."""
if self._sandbox_id is None:
raise RuntimeError("Sandbox ID not initialized. Call _ensure_sandbox() first.")
return self._sandbox_id
2025-04-09 20:46:13 +08:00
def clean_path(self, path: str) -> str:
2025-04-23 17:53:38 +08:00
"""Clean and normalize a path to be relative to /workspace."""
2025-04-13 05:40:01 +08:00
cleaned_path = clean_path(path, self.workspace_path)
2025-04-10 00:44:17 +08:00
logger.debug(f"Cleaned path: {path} -> {cleaned_path}")
2025-04-18 23:58:08 +08:00
return cleaned_path