mirror of https://github.com/kortix-ai/suna.git
add sandbox api AUTH
This commit is contained in:
parent
76163f6cc3
commit
6fea355191
|
@ -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():
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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<void> => {
|
||||
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;
|
||||
|
|
Loading…
Reference in New Issue