add sandbox api AUTH

This commit is contained in:
Adam Cohen Hillel 2025-04-18 15:04:11 +01:00
parent 76163f6cc3
commit 6fea355191
3 changed files with 129 additions and 19 deletions

View File

@ -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():

View File

@ -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)

View File

@ -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;