suna/backend/agent/tools/sb_deploy_tool.py

145 lines
6.2 KiB
Python

import os
from dotenv import load_dotenv
from agentpress.tool import ToolResult, openapi_schema, xml_schema
from sandbox.sandbox import SandboxToolsBase, Sandbox
from utils.files_utils import clean_path
from agent.tools.sb_shell_tool import SandboxShellTool
# Load environment variables
load_dotenv()
class SandboxDeployTool(SandboxToolsBase):
"""Tool for deploying static websites from a Daytona sandbox to Cloudflare Pages."""
def __init__(self, sandbox: Sandbox):
super().__init__(sandbox)
self.workspace_path = "/workspace" # Ensure we're always operating in /workspace
self.cloudflare_api_token = os.getenv("CLOUDFLARE_API_TOKEN")
self.shell_tool = SandboxShellTool(sandbox)
def clean_path(self, path: str) -> str:
"""Clean and normalize a path to be relative to /workspace"""
return clean_path(path, self.workspace_path)
@openapi_schema({
"type": "function",
"function": {
"name": "deploy",
"description": "Deploy a static website (HTML+CSS+JS) from a directory in the sandbox to Cloudflare Pages. Only use this tool when permanent deployment to a production environment is needed. The directory path must be relative to /workspace. The website will be deployed to {name}.kortix.cloud.",
"parameters": {
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Name for the deployment, will be used in the URL as {name}.kortix.cloud"
},
"directory_path": {
"type": "string",
"description": "Path to the directory containing the static website files to deploy, relative to /workspace (e.g., 'build')"
}
},
"required": ["name", "directory_path"]
}
}
})
@xml_schema(
tag_name="deploy",
mappings=[
{"param_name": "name", "node_type": "attribute", "path": "name"},
{"param_name": "directory_path", "node_type": "attribute", "path": "directory_path"}
],
example='''
<!--
IMPORTANT: Only use this tool when:
1. The user explicitly requests permanent deployment to production
2. You have a complete, ready-to-deploy directory
NOTE: If the same name is used, it will redeploy to the same project as before
-->
<deploy name="my-site" directory_path="website">
</deploy>
'''
)
async def deploy(self, name: str, directory_path: str) -> ToolResult:
"""
Deploy a static website (HTML+CSS+JS) from the sandbox to Cloudflare Pages.
Only use this tool when permanent deployment to a production environment is needed.
Args:
name: Name for the deployment, will be used in the URL as {name}.kortix.cloud
directory_path: Path to the directory to deploy, relative to /workspace
Returns:
ToolResult containing:
- Success: Deployment information including URL
- Failure: Error message if deployment fails
"""
try:
directory_path = self.clean_path(directory_path)
full_path = f"{self.workspace_path}/{directory_path}"
# Verify the directory exists
try:
dir_info = self.sandbox.fs.get_file_info(full_path)
if not dir_info.is_dir:
return self.fail_response(f"'{directory_path}' is not a directory")
except Exception as e:
return self.fail_response(f"Directory '{directory_path}' does not exist: {str(e)}")
# Deploy to Cloudflare Pages directly from the container
try:
# Get Cloudflare API token from environment
if not self.cloudflare_api_token:
return self.fail_response("CLOUDFLARE_API_TOKEN environment variable not set")
# Single command that creates the project if it doesn't exist and then deploys
project_name = f"{self.sandbox_id}-{name}"
deploy_cmd = f'''export CLOUDFLARE_API_TOKEN={self.cloudflare_api_token} &&
(npx wrangler pages deploy {full_path} --project-name {project_name} ||
(npx wrangler pages project create {project_name} --production-branch production &&
npx wrangler pages deploy {full_path} --project-name {project_name}))'''
# Execute command using shell_tool.execute_command
response = await self.shell_tool.execute_command(
command=deploy_cmd,
folder=None, # Use the workspace root
timeout=300 # Increased timeout for deployments
)
print(f"Deployment response: {response}")
if response.success:
return self.success_response({
"message": f"Website deployed successfully",
"output": response.output
})
else:
return self.fail_response(f"Deployment failed: {response.output}")
except Exception as e:
return self.fail_response(f"Error during deployment: {str(e)}")
except Exception as e:
return self.fail_response(f"Error deploying website: {str(e)}")
if __name__ == "__main__":
import asyncio
import sys
async def test_deploy():
# Replace these with actual values for testing
sandbox_id = "sandbox-ccb30b35"
password = "test-password"
# Initialize the deploy tool
deploy_tool = SandboxDeployTool(sandbox_id, password)
# Test deployment - replace with actual directory path and site name
result = await deploy_tool.deploy(
name="test-site-1x",
directory_path="website" # Directory containing static site files
)
print(f"Deployment result: {result}")
asyncio.run(test_deploy())