Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions backend/app/api/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,9 @@
from app.api.endpoints.internal import (
callback_router,
chat_storage_router,
)
from app.api.endpoints.internal import devices_router as internal_devices_router
from app.api.endpoints.internal import (
services_router,
skills_router,
subscriptions_router,
Expand Down Expand Up @@ -175,6 +178,9 @@

api_router.include_router(skills_router, prefix="/internal", tags=["internal-skills"])
api_router.include_router(tables_router, prefix="/internal", tags=["internal-tables"])
api_router.include_router(
internal_devices_router, prefix="/internal", tags=["internal-devices"]
)
api_router.include_router(
internal_bots_router, prefix="/internal", tags=["internal-bots"]
)
Expand Down
77 changes: 77 additions & 0 deletions backend/app/api/endpoints/devices.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,42 @@ class DeviceUpgradeResponse(BaseModel):
message: str = Field(..., description="Human-readable status message")


class DeviceSandboxExecRequest(BaseModel):
"""Request model for executing a command on a user device."""

command: str = Field(..., min_length=1, description="Command to execute")
working_dir: str = Field(
default="/home/user",
description="Working directory for command execution",
)
timeout_seconds: int = Field(
default=300,
ge=1,
le=1800,
description="Command timeout in seconds",
)
required_capability: Optional[str] = Field(
default=None,
description="Optional device capability required for routing",
)
device_id: Optional[str] = Field(
default=None,
description="Optional explicit device ID override",
)


class DeviceSandboxExecResponse(BaseModel):
"""Response model for a device-backed command execution."""

success: bool = Field(..., description="Whether the command succeeded")
stdout: str = Field(default="", description="Standard output")
stderr: str = Field(default="", description="Standard error")
exit_code: int = Field(..., description="Process exit code")
execution_time: float = Field(..., description="Execution time in seconds")
device_id: str = Field(..., description="Device that executed the command")
backend: str = Field(default="device", description="Execution backend identifier")


@router.get("", response_model=DeviceListResponse)
async def get_all_devices(
db: Session = Depends(get_db),
Expand Down Expand Up @@ -162,6 +198,47 @@ async def delete_device(
return {"message": f"Device '{device_id}' deleted"}


@router.post("/sandbox/exec", response_model=DeviceSandboxExecResponse)
async def execute_device_sandbox_command(
request: DeviceSandboxExecRequest,
db: Session = Depends(get_db),
current_user: User = Depends(security.get_current_user),
) -> DeviceSandboxExecResponse:
"""
Execute a command on an online user device through the existing device channel.

The backend selects a compatible online device, forwards the command over
`/local-executor`, and returns the device's execution result.
"""
from app.services.device_sandbox_service import (
DeviceSandboxError,
device_sandbox_service,
)

try:
result = await device_sandbox_service.execute_command(
db=db,
user_id=current_user.id,
command=request.command,
working_dir=request.working_dir,
timeout_seconds=request.timeout_seconds,
required_capability=request.required_capability,
device_id=request.device_id,
)
except DeviceSandboxError as exc:
logger.warning(
"[Device Sandbox] Command rejected: user_id=%s, error=%s",
current_user.id,
exc,
)
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(exc),
) from exc

return DeviceSandboxExecResponse(**result)


@router.post("/{device_id}/upgrade", response_model=DeviceUpgradeResponse)
async def trigger_device_upgrade(
device_id: str,
Expand Down
2 changes: 2 additions & 0 deletions backend/app/api/endpoints/internal/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
from .bots import router as bots_router
from .callback import router as callback_router
from .chat_storage import router as chat_storage_router
from .devices import router as devices_router
from .services import router as services_router
from .skills import router as skills_router
from .subscriptions import router as subscriptions_router
Expand All @@ -23,6 +24,7 @@
"bots_router",
"callback_router",
"chat_storage_router",
"devices_router",
"services_router",
"skills_router",
"subscriptions_router",
Expand Down
Loading