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
|
# Import the agent API module
|
||||||
from agent import api as agent_api
|
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 environment variables
|
||||||
load_dotenv()
|
load_dotenv()
|
||||||
|
@ -36,6 +36,9 @@ async def lifespan(app: FastAPI):
|
||||||
instance_id # Pass the instance_id to agent_api
|
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
|
# Initialize Redis before restoring agent runs
|
||||||
from services import redis
|
from services import redis
|
||||||
await redis.initialize_async()
|
await redis.initialize_async()
|
||||||
|
@ -66,7 +69,7 @@ app.add_middleware(
|
||||||
app.include_router(agent_api.router, prefix="/api")
|
app.include_router(agent_api.router, prefix="/api")
|
||||||
|
|
||||||
# Include the sandbox router with a prefix
|
# 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")
|
@app.get("/api/health-check")
|
||||||
async def health_check():
|
async def health_check():
|
||||||
|
|
|
@ -1,15 +1,27 @@
|
||||||
import os
|
import os
|
||||||
from typing import List, Optional
|
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 fastapi.responses import Response, JSONResponse
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from utils.logger import logger
|
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 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
|
# 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):
|
class FileInfo(BaseModel):
|
||||||
"""Model for file information"""
|
"""Model for file information"""
|
||||||
name: str
|
name: str
|
||||||
|
@ -19,16 +31,51 @@ class FileInfo(BaseModel):
|
||||||
mod_time: str
|
mod_time: str
|
||||||
permissions: Optional[str] = None
|
permissions: Optional[str] = None
|
||||||
|
|
||||||
# Create a router for the Sandbox API
|
async def verify_sandbox_access(client, sandbox_id: str, user_id: str):
|
||||||
router = APIRouter(tags=["sandbox"])
|
"""
|
||||||
|
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")
|
@router.post("/sandboxes/{sandbox_id}/files")
|
||||||
async def create_file(
|
async def create_file(
|
||||||
sandbox_id: str,
|
sandbox_id: str,
|
||||||
path: str = Form(...),
|
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"""
|
"""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:
|
try:
|
||||||
# Get or start sandbox instance
|
# Get or start sandbox instance
|
||||||
sandbox = await get_or_start_sandbox(sandbox_id)
|
sandbox = await get_or_start_sandbox(sandbox_id)
|
||||||
|
@ -47,8 +94,17 @@ async def create_file(
|
||||||
|
|
||||||
# For backward compatibility, keep the JSON version too
|
# For backward compatibility, keep the JSON version too
|
||||||
@router.post("/sandboxes/{sandbox_id}/files/json")
|
@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)"""
|
"""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:
|
try:
|
||||||
# Get or start sandbox instance
|
# Get or start sandbox instance
|
||||||
sandbox = await get_or_start_sandbox(sandbox_id)
|
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))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@router.get("/sandboxes/{sandbox_id}/files")
|
@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"""
|
"""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:
|
try:
|
||||||
# Get or start sandbox instance using the async function
|
# Get or start sandbox instance using the async function
|
||||||
sandbox = await get_or_start_sandbox(sandbox_id)
|
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))
|
raise HTTPException(status_code=500, detail=str(e))
|
||||||
|
|
||||||
@router.get("/sandboxes/{sandbox_id}/files/content")
|
@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"""
|
"""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:
|
try:
|
||||||
# Get or start sandbox instance using the async function
|
# Get or start sandbox instance using the async function
|
||||||
sandbox = await get_or_start_sandbox(sandbox_id)
|
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');
|
throw new Error('No access token available');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine if content is likely binary (contains non-printable characters)
|
// Use FormData to handle both text and binary content more reliably
|
||||||
const isProbablyBinary = /[\x00-\x08\x0E-\x1F\x80-\xFF]/.test(content) ||
|
const formData = new FormData();
|
||||||
content.startsWith('data:') ||
|
formData.append('path', filePath);
|
||||||
/^[A-Za-z0-9+/]*={0,2}$/.test(content);
|
|
||||||
|
// 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`, {
|
const response = await fetch(`${API_URL}/sandboxes/${sandboxId}/files`, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
headers: {
|
headers: {
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'Authorization': `Bearer ${session.access_token}`,
|
'Authorization': `Bearer ${session.access_token}`,
|
||||||
},
|
},
|
||||||
body: JSON.stringify({
|
body: formData,
|
||||||
path: filePath,
|
|
||||||
content: content,
|
|
||||||
is_base64: isProbablyBinary
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!response.ok) {
|
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 {
|
export interface FileInfo {
|
||||||
name: string;
|
name: string;
|
||||||
path: string;
|
path: string;
|
||||||
|
|
Loading…
Reference in New Issue