Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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