add sandbox runtime control endpoints

This commit is contained in:
Азамат Нураев 2026-04-28 21:53:26 +03:00
parent 0ca0bac9bf
commit 1b38bcfeab
17 changed files with 1408 additions and 119 deletions

View file

@ -1,7 +1,7 @@
from fastapi import Depends, Request
from adapter.di.container import AppContainer
from usecase.sandbox import CreateSandbox
from usecase.sandbox import CreateSandbox, DeleteSandbox
APP_CONTAINER_STATE = 'container'
APP_CONFIG_STATE = 'config'
@ -18,3 +18,9 @@ def get_create_sandbox(
container: AppContainer = Depends(get_container),
) -> CreateSandbox:
return container.usecases.create_sandbox
def get_delete_sandbox(
container: AppContainer = Depends(get_container),
) -> DeleteSandbox:
return container.usecases.delete_sandbox

View file

@ -1,19 +1,30 @@
from uuid import UUID
from fastapi import APIRouter, Depends, HTTPException, status
from adapter.di.container import AppContainer
from adapter.http.fastapi.dependencies import (
get_container,
get_create_sandbox,
get_delete_sandbox,
)
from adapter.http.fastapi.schemas import (
CreateSandboxRequest,
DeleteSandboxResponse,
ErrorResponse,
HealthResponse,
SandboxEndpointResponse,
SandboxSessionResponse,
)
from domain.error import SandboxError, SandboxStartError
from domain.error import SandboxConflictError, SandboxError, SandboxStartError
from domain.sandbox import SandboxSession
from usecase.sandbox import CreateSandbox, CreateSandboxCommand
from usecase.sandbox import (
CreateSandbox,
CreateSandboxCommand,
DeleteSandbox,
DeleteSandboxCommand,
DeleteSandboxResult,
)
router = APIRouter()
@ -35,6 +46,7 @@ def health(container: AppContainer = Depends(get_container)) -> HealthResponse:
'/create',
response_model=SandboxSessionResponse,
responses={
status.HTTP_409_CONFLICT: {'model': ErrorResponse},
status.HTTP_503_SERVICE_UNAVAILABLE: {'model': ErrorResponse},
status.HTTP_500_INTERNAL_SERVER_ERROR: {'model': ErrorResponse},
},
@ -45,7 +57,18 @@ def create_sandbox(
usecase: CreateSandbox = Depends(get_create_sandbox),
) -> SandboxSessionResponse:
try:
session = usecase.execute(CreateSandboxCommand(chat_id=request.chat_id))
session = usecase.execute(
CreateSandboxCommand(
chat_id=request.chat_id,
agent_id=request.agent_id,
volume_host_path=request.volume_host_path,
)
)
except SandboxConflictError as exc:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=str(exc),
) from exc
except SandboxStartError as exc:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
@ -60,11 +83,55 @@ def create_sandbox(
return _to_sandbox_session_response(session)
@router.delete(
'/sandboxes/{chat_id}',
response_model=DeleteSandboxResponse,
responses={
status.HTTP_500_INTERNAL_SERVER_ERROR: {'model': ErrorResponse},
},
status_code=status.HTTP_200_OK,
)
def delete_sandbox(
chat_id: UUID,
usecase: DeleteSandbox = Depends(get_delete_sandbox),
) -> DeleteSandboxResponse:
try:
result = usecase.execute(DeleteSandboxCommand(chat_id=chat_id))
except SandboxError as exc:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=str(exc),
) from exc
return _to_delete_sandbox_response(result)
def _to_sandbox_session_response(session: SandboxSession) -> SandboxSessionResponse:
if session.endpoint is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail='sandbox_endpoint_unavailable',
)
return SandboxSessionResponse(
session_id=session.session_id,
chat_id=session.chat_id,
agent_id=session.agent_id,
volume_host_path=session.volume_host_path,
container_id=session.container_id,
endpoint=SandboxEndpointResponse(
ip=session.endpoint.ip,
port=session.endpoint.port,
),
status=session.status.value,
expires_at=session.expires_at,
)
def _to_delete_sandbox_response(result: DeleteSandboxResult) -> DeleteSandboxResponse:
return DeleteSandboxResponse(
chat_id=result.chat_id,
result=result.result,
session_id=result.session_id,
container_id=result.container_id,
)

View file

@ -1,7 +1,8 @@
from datetime import datetime
from pathlib import Path
from uuid import UUID
from pydantic import BaseModel, ConfigDict
from pydantic import BaseModel, ConfigDict, field_validator
class HealthResponse(BaseModel):
@ -14,15 +15,47 @@ class CreateSandboxRequest(BaseModel):
model_config = ConfigDict(extra='forbid')
chat_id: UUID
agent_id: str
volume_host_path: str
@field_validator('agent_id')
@classmethod
def validate_agent_id(cls, value: str) -> str:
if not value.strip():
raise ValueError('invalid agent_id')
return value
@field_validator('volume_host_path')
@classmethod
def validate_volume_host_path(cls, value: str) -> str:
path = Path(value).expanduser()
if not path.is_absolute():
raise ValueError('invalid volume_host_path')
return str(path.resolve(strict=False))
class SandboxEndpointResponse(BaseModel):
ip: str
port: int
class SandboxSessionResponse(BaseModel):
session_id: UUID
chat_id: UUID
agent_id: str
volume_host_path: str
container_id: str
endpoint: SandboxEndpointResponse
status: str
expires_at: datetime
class DeleteSandboxResponse(BaseModel):
chat_id: UUID
result: str
session_id: UUID | None = None
container_id: str | None = None
class ErrorResponse(BaseModel):
detail: str