diff --git a/backend/api.py b/backend/api.py index f8fcc935..84cce089 100644 --- a/backend/api.py +++ b/backend/api.py @@ -11,7 +11,7 @@ import uuid # Import the agent API module from agent import api as agent_api -from sandbox.api import router as sandbox_router +from sandbox import api as sandbox_api # Load environment variables load_dotenv() @@ -36,6 +36,9 @@ async def lifespan(app: FastAPI): instance_id # Pass the instance_id to agent_api ) + # Initialize the sandbox API with shared resources + sandbox_api.initialize(db) + # Initialize Redis before restoring agent runs from services import redis await redis.initialize_async() @@ -66,7 +69,7 @@ app.add_middleware( app.include_router(agent_api.router, prefix="/api") # Include the sandbox router with a prefix -app.include_router(sandbox_router, prefix="/api") +app.include_router(sandbox_api.router, prefix="/api") @app.get("/api/health-check") async def health_check(): diff --git a/backend/sandbox/api.py b/backend/sandbox/api.py index 2cdd3386..f539fb4b 100644 --- a/backend/sandbox/api.py +++ b/backend/sandbox/api.py @@ -1,15 +1,27 @@ import os from typing import List, Optional -from fastapi import FastAPI, UploadFile, File, HTTPException, APIRouter, Form +from fastapi import FastAPI, UploadFile, File, HTTPException, APIRouter, Form, Depends, Request from fastapi.responses import Response, JSONResponse from pydantic import BaseModel from utils.logger import logger +from utils.auth_utils import get_current_user_id, get_user_id_from_stream_auth from sandbox.sandbox import get_or_start_sandbox +from services.supabase import DBConnection # TODO: ADD AUTHORIZATION TO ONLY HAVE ACCESS TO SANDBOXES OF PROJECTS U HAVE ACCESS TO +# Initialize shared resources +router = APIRouter(tags=["sandbox"]) +db = None + +def initialize(_db: DBConnection): + """Initialize the sandbox API with resources from the main API.""" + global db + db = _db + logger.info("Initialized sandbox API with database connection") + class FileInfo(BaseModel): """Model for file information""" name: str @@ -19,16 +31,51 @@ class FileInfo(BaseModel): mod_time: str permissions: Optional[str] = None -# Create a router for the Sandbox API -router = APIRouter(tags=["sandbox"]) +async def verify_sandbox_access(client, sandbox_id: str, user_id: str): + """ + Verify that a user has access to a specific sandbox based on account membership. + + Args: + client: The Supabase client + sandbox_id: The sandbox ID to check access for + user_id: The user ID to check permissions for + + Returns: + dict: Project data containing sandbox information + + Raises: + HTTPException: If the user doesn't have access to the sandbox or sandbox doesn't exist + """ + # Find the project that owns this sandbox + project_result = await client.table('projects').select('*').filter('sandbox->>id', 'eq', sandbox_id).execute() + + if not project_result.data or len(project_result.data) == 0: + raise HTTPException(status_code=404, detail="Sandbox not found") + + project_data = project_result.data[0] + account_id = project_data.get('account_id') + + # Verify account membership + if account_id: + account_user_result = await client.schema('basejump').from_('account_user').select('account_role').eq('user_id', user_id).eq('account_id', account_id).execute() + if account_user_result.data and len(account_user_result.data) > 0: + return project_data + + raise HTTPException(status_code=403, detail="Not authorized to access this sandbox") @router.post("/sandboxes/{sandbox_id}/files") async def create_file( sandbox_id: str, path: str = Form(...), - file: UploadFile = File(...) + file: UploadFile = File(...), + user_id: str = Depends(get_current_user_id) ): """Create a file in the sandbox using direct file upload""" + client = await db.client + + # Verify the user has access to this sandbox + await verify_sandbox_access(client, sandbox_id, user_id) + try: # Get or start sandbox instance sandbox = await get_or_start_sandbox(sandbox_id) @@ -47,8 +94,17 @@ async def create_file( # For backward compatibility, keep the JSON version too @router.post("/sandboxes/{sandbox_id}/files/json") -async def create_file_json(sandbox_id: str, file_request: dict): +async def create_file_json( + sandbox_id: str, + file_request: dict, + user_id: str = Depends(get_current_user_id) +): """Create a file in the sandbox using JSON (legacy support)""" + client = await db.client + + # Verify the user has access to this sandbox + await verify_sandbox_access(client, sandbox_id, user_id) + try: # Get or start sandbox instance sandbox = await get_or_start_sandbox(sandbox_id) @@ -74,8 +130,17 @@ async def create_file_json(sandbox_id: str, file_request: dict): raise HTTPException(status_code=500, detail=str(e)) @router.get("/sandboxes/{sandbox_id}/files") -async def list_files(sandbox_id: str, path: str): +async def list_files( + sandbox_id: str, + path: str, + user_id: str = Depends(get_current_user_id) +): """List files and directories at the specified path""" + client = await db.client + + # Verify the user has access to this sandbox + await verify_sandbox_access(client, sandbox_id, user_id) + try: # Get or start sandbox instance using the async function sandbox = await get_or_start_sandbox(sandbox_id) @@ -102,8 +167,17 @@ async def list_files(sandbox_id: str, path: str): raise HTTPException(status_code=500, detail=str(e)) @router.get("/sandboxes/{sandbox_id}/files/content") -async def read_file(sandbox_id: str, path: str): +async def read_file( + sandbox_id: str, + path: str, + user_id: str = Depends(get_current_user_id) +): """Read a file from the sandbox""" + client = await db.client + + # Verify the user has access to this sandbox + await verify_sandbox_access(client, sandbox_id, user_id) + try: # Get or start sandbox instance using the async function sandbox = await get_or_start_sandbox(sandbox_id) diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts index 42e744a9..a09c3f1f 100644 --- a/frontend/src/lib/api.ts +++ b/frontend/src/lib/api.ts @@ -744,22 +744,20 @@ export const createSandboxFile = async (sandboxId: string, filePath: string, con throw new Error('No access token available'); } - // Determine if content is likely binary (contains non-printable characters) - const isProbablyBinary = /[\x00-\x08\x0E-\x1F\x80-\xFF]/.test(content) || - content.startsWith('data:') || - /^[A-Za-z0-9+/]*={0,2}$/.test(content); + // Use FormData to handle both text and binary content more reliably + const formData = new FormData(); + formData.append('path', filePath); + // Create a Blob from the content string and append as a file + const blob = new Blob([content], { type: 'application/octet-stream' }); + formData.append('file', blob, filePath.split('/').pop() || 'file'); + const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, { method: 'POST', headers: { - 'Content-Type': 'application/json', 'Authorization': `Bearer ${session.access_token}`, }, - body: JSON.stringify({ - path: filePath, - content: content, - is_base64: isProbablyBinary - }), + body: formData, }); if (!response.ok) { @@ -775,6 +773,41 @@ export const createSandboxFile = async (sandboxId: string, filePath: string, con } }; +// Fallback method for legacy support using JSON +export const createSandboxFileJson = async (sandboxId: string, filePath: string, content: string): Promise => { + try { + const supabase = createClient(); + const { data: { session } } = await supabase.auth.getSession(); + + if (!session?.access_token) { + throw new Error('No access token available'); + } + + const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files/json`, { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'Authorization': `Bearer ${session.access_token}`, + }, + body: JSON.stringify({ + path: filePath, + content: content + }), + }); + + if (!response.ok) { + const errorText = await response.text().catch(() => 'No error details available'); + console.error(`Error creating sandbox file (JSON): ${response.status} ${response.statusText}`, errorText); + throw new Error(`Error creating sandbox file: ${response.statusText} (${response.status})`); + } + + return response.json(); + } catch (error) { + console.error('Failed to create sandbox file with JSON:', error); + throw error; + } +}; + export interface FileInfo { name: string; path: string;