Repository navigation
feat(devices): add cloud device connection controls #1187
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -438,3 +438,100 @@ async def execute_device_command( | |
| result.get("duration"), | ||
| ) | ||
| return DeviceCommandResponse(**result) | ||
|
|
||
|
|
||
| # ==================== Device Session Endpoints ==================== | ||
|
|
||
| DEFAULT_DEVICE_SESSION_PATH = "/home/ubuntu/.wegent-executor/workspace" | ||
|
|
||
|
|
||
| class DeviceSessionResponse(BaseModel): | ||
| """Response model for device session creation.""" | ||
|
|
||
| session_id: str = Field(..., description="Unique session identifier") | ||
| device_id: str = Field(..., description="Target device ID") | ||
| type: str = Field(..., description="Session type (terminal or code_server)") | ||
| path: str = Field(..., description="Working directory path") | ||
| url: str = Field(default="", description="Browser-accessible session URL") | ||
|
|
||
|
|
||
| @router.post("/{device_id}/terminal", response_model=DeviceSessionResponse) | ||
| async def start_device_terminal( | ||
| device_id: str, | ||
| db: Session = Depends(get_db), | ||
| current_user: User = Depends(security.get_current_user), | ||
| ): | ||
| """Start a terminal session on a device. | ||
|
|
||
| Sends a Socket.IO RPC to the online device executor to start a ttyd | ||
| terminal session. Requires the device to be online. | ||
| """ | ||
| from app.services.device.session_service import ( | ||
| DeviceSessionError, | ||
| local_device_session_service, | ||
| ) | ||
|
|
||
| try: | ||
| result = await local_device_session_service.start_session( | ||
| db=db, | ||
| user_id=current_user.id, | ||
| device_id=device_id, | ||
| project_id=0, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win Extract The literal ♻️ Proposed refactor DEFAULT_DEVICE_SESSION_PATH = "/home/ubuntu/.wegent-executor/workspace"
+DEFAULT_DEVICE_PROJECT_ID = 0
...
- project_id=0,
+ project_id=DEFAULT_DEVICE_PROJECT_ID,
...
- project_id=0,
+ project_id=DEFAULT_DEVICE_PROJECT_ID,As per coding guidelines, extract magic numbers to named constants in Python code. Also applies to: 520-520 🤖 Prompt for AI Agents |
||
| session_type="terminal", | ||
| path=DEFAULT_DEVICE_SESSION_PATH, | ||
| create_if_missing=True, | ||
| ) | ||
| except DeviceSessionError as exc: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_400_BAD_REQUEST, | ||
| detail=str(exc), | ||
| ) | ||
|
Comment on lines
+485
to
+488
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Preserve exception causality when mapping session errors to HTTP errors. Use exception chaining so traceback clearly shows the original 🛠️ Proposed fix except DeviceSessionError as exc:
- raise HTTPException(
+ raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(exc),
- )
+ ) from exc
...
except DeviceSessionError as exc:
- raise HTTPException(
+ raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail=str(exc),
- )
+ ) from excAlso applies to: 526-529 🧰 Tools🪛 Ruff (0.15.14)[warning] 485-488: Within an (B904) 🤖 Prompt for AI Agents |
||
|
|
||
| return DeviceSessionResponse( | ||
| session_id=result.get("session_id", ""), | ||
| device_id=result.get("device_id", device_id), | ||
| type="terminal", | ||
| path=result.get("path", DEFAULT_DEVICE_SESSION_PATH), | ||
| url=result.get("url", ""), | ||
| ) | ||
|
|
||
|
|
||
| @router.post("/{device_id}/code-server", response_model=DeviceSessionResponse) | ||
| async def start_device_code_server( | ||
| device_id: str, | ||
| db: Session = Depends(get_db), | ||
| current_user: User = Depends(security.get_current_user), | ||
| ): | ||
| """Start a code-server (IDE) session on a device. | ||
|
|
||
| Sends a Socket.IO RPC to the online device executor to start a | ||
| code-server session. Requires the device to be online. | ||
| """ | ||
| from app.services.device.session_service import ( | ||
| DeviceSessionError, | ||
| local_device_session_service, | ||
| ) | ||
|
|
||
| try: | ||
| result = await local_device_session_service.start_session( | ||
| db=db, | ||
| user_id=current_user.id, | ||
| device_id=device_id, | ||
| project_id=0, | ||
| session_type="code_server", | ||
| path=DEFAULT_DEVICE_SESSION_PATH, | ||
| create_if_missing=True, | ||
| ) | ||
| except DeviceSessionError as exc: | ||
| raise HTTPException( | ||
| status_code=status.HTTP_400_BAD_REQUEST, | ||
| detail=str(exc), | ||
| ) | ||
|
|
||
| return DeviceSessionResponse( | ||
| session_id=result.get("session_id", ""), | ||
| device_id=result.get("device_id", device_id), | ||
| type="code_server", | ||
| path=result.get("path", DEFAULT_DEVICE_SESSION_PATH), | ||
| url=result.get("url", ""), | ||
| ) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| import { describe, expect, test, vi } from 'vitest' | ||
| import { createDeviceApi } from './devices' | ||
| import type { HttpClient } from './http' | ||
|
|
||
| describe('createDeviceApi', () => { | ||
| test('uses cloud device endpoints for restart and delete actions', async () => { | ||
| const client = { | ||
| delete: vi.fn().mockResolvedValue({ message: 'deleted' }), | ||
| post: vi.fn().mockResolvedValue({ message: 'restart sent' }), | ||
| } as unknown as HttpClient | ||
|
|
||
| const api = createDeviceApi(client) | ||
|
|
||
| await api.restartCloudDevice('device/1') | ||
| await api.deleteCloudDevice('device/1') | ||
|
|
||
| expect(client.post).toHaveBeenCalledWith('/cloud-devices/device%2F1/restart') | ||
| expect(client.delete).toHaveBeenCalledWith('/cloud-devices/device%2F1') | ||
| }) | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win
Add return type hints and async tracing decorators on the new endpoints.
Both new async handlers are missing explicit return annotations and endpoint-level tracing decorators.
As per coding guidelines, use type hints in Python code and apply
@trace_async()to entire async functions.Also applies to: 499-504
🧰 Tools
🪛 Ruff (0.15.14)
[warning] 461-461: Do not perform function call
Dependsin argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable(B008)
[warning] 462-462: Do not perform function call
Dependsin argument defaults; instead, perform the call within the function, or read the default from a module-level singleton variable(B008)
🤖 Prompt for AI Agents