نسخهٔ وب مستقل — پلتفرم نیروی کار هوش مصنوعی

مخزن اختصاصی برنامهٔ وب؛ محتوای پوشهٔ web در ریشهٔ مخزن قرار گرفت.
تاریخچهٔ پروژهٔ ربات (agy-bot) در این مخزن وجود ندارد و هیچ وابستگی runtime به آن نیست.
This commit is contained in:
mjalmoudy94
2026-10-13 00:39:28 +03:30
commit 717e53a7ce
185 changed files with 39910 additions and 0 deletions
@@ -0,0 +1,449 @@
"""Sandbox Gateway endpoints.
Note what is **absent**: there is no route that accepts a shell command and runs
it on the host. Every execution goes to ``SandboxProviderPort``, is bounded by a
policy timeout and output cap, and is metered into the usage ledger.
Running a command requires ``sandbox.use``; tearing a workspace down requires
``projects.manage`` because it destroys work.
"""
from __future__ import annotations
from datetime import datetime
from fastapi import APIRouter, Depends, Query, Request, status
from pydantic import BaseModel, Field
from sqlalchemy.ext.asyncio import AsyncSession
from app.application.services.audit_service import record_audit
from app.application.services.sandbox_service import SandboxService
from app.core.database import get_session
from app.domain.audit.models import AuditAction
from app.domain.identity.models import CAN_MANAGE_PROJECTS, CAN_USE_SANDBOX
from app.domain.sandbox.models import NetworkPolicy
from app.interfaces.http.deps import (
OrgContext,
client_meta,
get_org_context,
get_sandbox_service,
)
router = APIRouter(prefix="/organizations/{org_id}/sandbox", tags=["sandbox"])
# ---------------------------------------------------------------------------
# Schemas
# ---------------------------------------------------------------------------
class SandboxTemplateOut(BaseModel):
id: str
name: str
provider: str
image: str
description: str
default_cpu: float
default_memory_mb: int
default_disk_mb: int
class WorkspaceOut(BaseModel):
id: str
project_id: str
template_id: str
provider: str
status: str
created_at: datetime
class CreateWorkspaceRequest(BaseModel):
project_id: str
template_id: str
class SessionOut(BaseModel):
id: str
workspace_id: str
status: str
started_at: datetime
last_active_at: datetime
class StartSessionRequest(BaseModel):
workspace_id: str
class CommandRunOut(BaseModel):
id: str
command: str
exit_code: int | None
stdout: str
stderr: str
duration_ms: int
truncated: bool
created_at: datetime
class RunCommandRequest(BaseModel):
command: str = Field(min_length=1, max_length=4_000)
class FileReadOut(BaseModel):
path: str
content: str
class ReadFileRequest(BaseModel):
path: str = Field(min_length=1, max_length=1_000)
class WriteFileRequest(BaseModel):
path: str = Field(min_length=1, max_length=1_000)
content: str = Field(max_length=1_000_000)
class SnapshotOut(BaseModel):
id: str
name: str
created_at: datetime
class CreateSnapshotRequest(BaseModel):
name: str = Field(min_length=1, max_length=120)
class PreviewUrlOut(BaseModel):
port: int
url: str
class ResourceLimitOut(BaseModel):
cpu: float
memory_mb: int
disk_mb: int
timeout_seconds: int
max_output_bytes: int
network_policy: str
class SetLimitsRequest(BaseModel):
scope: str = "organization"
scope_id: str | None = None
cpu: float = Field(default=1.0, gt=0, le=16)
memory_mb: int = Field(default=1024, ge=128, le=65_536)
disk_mb: int = Field(default=2048, ge=256, le=1_048_576)
timeout_seconds: int = Field(default=120, ge=1, le=900)
max_output_bytes: int = Field(default=200_000, ge=1024, le=2_000_000)
network_policy: NetworkPolicy = NetworkPolicy.DENY_ALL
# ---------------------------------------------------------------------------
# Templates & limits
# ---------------------------------------------------------------------------
@router.get("/templates", response_model=list[SandboxTemplateOut])
async def list_templates(
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> list[SandboxTemplateOut]:
templates = await service.list_templates()
return [
SandboxTemplateOut(
id=t.id, name=t.name, provider=str(t.provider), image=t.image,
description=t.description, default_cpu=t.default_cpu,
default_memory_mb=t.default_memory_mb, default_disk_mb=t.default_disk_mb,
)
for t in templates
]
@router.get("/limits", response_model=ResourceLimitOut)
async def get_limits(
project_id: str | None = Query(default=None),
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> ResourceLimitOut:
limit = await service.effective_limits(
organization_id=context.org_id, project_id=project_id
)
return ResourceLimitOut(
cpu=limit.cpu, memory_mb=limit.memory_mb, disk_mb=limit.disk_mb,
timeout_seconds=limit.timeout_seconds, max_output_bytes=limit.max_output_bytes,
network_policy=str(limit.network_policy),
)
@router.put("/limits", response_model=ResourceLimitOut)
async def set_limits(
payload: SetLimitsRequest,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> ResourceLimitOut:
context.require(CAN_MANAGE_PROJECTS)
limit = await service.set_limits(
scope=payload.scope, scope_id=payload.scope_id or context.org_id,
cpu=payload.cpu, memory_mb=payload.memory_mb, disk_mb=payload.disk_mb,
timeout_seconds=payload.timeout_seconds,
max_output_bytes=payload.max_output_bytes,
network_policy=payload.network_policy,
)
return ResourceLimitOut(
cpu=limit.cpu, memory_mb=limit.memory_mb, disk_mb=limit.disk_mb,
timeout_seconds=limit.timeout_seconds, max_output_bytes=limit.max_output_bytes,
network_policy=str(limit.network_policy),
)
# ---------------------------------------------------------------------------
# Workspaces
# ---------------------------------------------------------------------------
@router.post(
"/workspaces", response_model=WorkspaceOut, status_code=status.HTTP_201_CREATED
)
async def create_workspace(
payload: CreateWorkspaceRequest,
request: Request,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
session: AsyncSession = Depends(get_session),
) -> WorkspaceOut:
context.require(CAN_USE_SANDBOX)
workspace = await service.create_workspace(
organization_id=context.org_id,
project_id=payload.project_id,
template_id=payload.template_id,
)
ip, ua = client_meta(request)
await record_audit(
session, action="sandbox.workspace_create", organization_id=context.org_id,
actor_user_id=context.user.id, target_type="workspace", target_id=workspace.id,
metadata={"project_id": workspace.project_id, "provider": str(workspace.provider)},
ip=ip, user_agent=ua,
)
return WorkspaceOut(
id=workspace.id, project_id=workspace.project_id,
template_id=workspace.template_id, provider=str(workspace.provider),
status=str(workspace.status), created_at=workspace.created_at,
)
@router.get("/workspaces", response_model=list[WorkspaceOut])
async def list_workspaces(
project_id: str = Query(min_length=1),
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
limit: int = Query(default=100, ge=1, le=500),
offset: int = Query(default=0, ge=0),
) -> list[WorkspaceOut]:
workspaces = await service.list_workspaces(
organization_id=context.org_id,
project_id=project_id,
limit=limit,
offset=offset,
)
return [
WorkspaceOut(
id=w.id, project_id=w.project_id, template_id=w.template_id,
provider=str(w.provider), status=str(w.status), created_at=w.created_at,
)
for w in workspaces
]
@router.delete("/workspaces/{workspace_id}", response_model=WorkspaceOut)
async def destroy_workspace(
workspace_id: str,
request: Request,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
session: AsyncSession = Depends(get_session),
) -> WorkspaceOut:
context.require(CAN_MANAGE_PROJECTS)
workspace = await service.destroy_workspace(
organization_id=context.org_id, workspace_id=workspace_id
)
ip, ua = client_meta(request)
await record_audit(
session, action=AuditAction.SANDBOX_DESTROY, organization_id=context.org_id,
actor_user_id=context.user.id, target_type="workspace",
target_id=workspace.id, ip=ip, user_agent=ua,
)
return WorkspaceOut(
id=workspace.id, project_id=workspace.project_id,
template_id=workspace.template_id, provider=str(workspace.provider),
status=str(workspace.status), created_at=workspace.created_at,
)
# ---------------------------------------------------------------------------
# Sessions
# ---------------------------------------------------------------------------
@router.post(
"/sessions", response_model=SessionOut, status_code=status.HTTP_201_CREATED
)
async def start_session(
payload: StartSessionRequest,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> SessionOut:
context.require(CAN_USE_SANDBOX)
sandbox_session = await service.start_session(
organization_id=context.org_id, workspace_id=payload.workspace_id
)
return SessionOut(
id=sandbox_session.id, workspace_id=sandbox_session.workspace_id,
status=str(sandbox_session.status), started_at=sandbox_session.started_at,
last_active_at=sandbox_session.last_active_at,
)
@router.post("/sessions/{session_id}/stop", response_model=SessionOut)
async def stop_session(
session_id: str,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> SessionOut:
context.require(CAN_USE_SANDBOX)
sandbox_session = await service.stop_session(
organization_id=context.org_id, session_id=session_id
)
return SessionOut(
id=sandbox_session.id, workspace_id=sandbox_session.workspace_id,
status=str(sandbox_session.status), started_at=sandbox_session.started_at,
last_active_at=sandbox_session.last_active_at,
)
# ---------------------------------------------------------------------------
# Commands & files
# ---------------------------------------------------------------------------
@router.post("/sessions/{session_id}/commands", response_model=CommandRunOut)
async def run_command(
session_id: str,
payload: RunCommandRequest,
request: Request,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
session: AsyncSession = Depends(get_session),
) -> CommandRunOut:
context.require(CAN_USE_SANDBOX)
run, _result = await service.run_command(
organization_id=context.org_id,
session_id=session_id,
command=payload.command,
user_id=context.user.id,
)
ip, ua = client_meta(request)
await record_audit(
session, action=AuditAction.SANDBOX_COMMAND, organization_id=context.org_id,
actor_user_id=context.user.id, target_type="sandbox_session",
target_id=session_id,
# The command itself is recorded, but never a secret from the environment.
metadata={"exit_code": run.exit_code, "duration_ms": run.duration_ms},
ip=ip, user_agent=ua,
)
return CommandRunOut(
id=run.id, command=run.command, exit_code=run.exit_code,
stdout=run.stdout_ref, stderr=run.stderr_ref, duration_ms=run.duration_ms,
truncated=run.truncated, created_at=run.created_at,
)
@router.get("/sessions/{session_id}/commands", response_model=list[CommandRunOut])
async def list_commands(
session_id: str,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> list[CommandRunOut]:
await service.get_session_or_404(
organization_id=context.org_id, session_id=session_id
)
runs = await service.list_commands(session_id=session_id)
return [
CommandRunOut(
id=r.id, command=r.command, exit_code=r.exit_code, stdout=r.stdout_ref,
stderr=r.stderr_ref, duration_ms=r.duration_ms, truncated=r.truncated,
created_at=r.created_at,
)
for r in runs
]
@router.post("/sessions/{session_id}/files/read", response_model=FileReadOut)
async def read_file(
session_id: str,
payload: ReadFileRequest,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> FileReadOut:
context.require(CAN_USE_SANDBOX)
content = await service.read_file(
organization_id=context.org_id, session_id=session_id, path=payload.path
)
return FileReadOut(path=payload.path, content=content)
@router.post("/sessions/{session_id}/files/write", status_code=status.HTTP_204_NO_CONTENT)
async def write_file(
session_id: str,
payload: WriteFileRequest,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> None:
context.require(CAN_USE_SANDBOX)
await service.write_file(
organization_id=context.org_id, session_id=session_id,
path=payload.path, content=payload.content,
)
# ---------------------------------------------------------------------------
# Snapshots & preview
# ---------------------------------------------------------------------------
@router.post(
"/sessions/{session_id}/snapshots",
response_model=SnapshotOut,
status_code=status.HTTP_201_CREATED,
)
async def create_snapshot(
session_id: str,
payload: CreateSnapshotRequest,
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> SnapshotOut:
context.require(CAN_USE_SANDBOX)
snapshot = await service.snapshot(
organization_id=context.org_id, session_id=session_id, name=payload.name
)
return SnapshotOut(
id=snapshot.id, name=snapshot.name, created_at=snapshot.created_at
)
@router.post("/sessions/{session_id}/preview-url", response_model=PreviewUrlOut)
async def get_preview_url(
session_id: str,
port: int = Query(ge=1, le=65535),
context: OrgContext = Depends(get_org_context),
service: SandboxService = Depends(get_sandbox_service),
) -> PreviewUrlOut:
context.require(CAN_USE_SANDBOX)
preview = await service.get_preview_url(
organization_id=context.org_id, session_id=session_id, port=port
)
return PreviewUrlOut(port=preview.port, url=preview.url)