annator-command-center / tools /device_tool.py
techprotrade's picture
Deploy ATOM FastAPI command center runtime (part 10)
a37e6db verified
Raw
History Blame Contribute Delete
42.7 kB
"""
Device Automation Tool
**✅ REAL IMPLEMENTATION (WebSocket-based)**
This module provides REAL device communication via WebSocket to React Native mobile apps.
No longer using mock implementations - all device functions communicate with actual devices.
Architecture:
- Backend (FastAPI) <--WebSocket--> Mobile App (React Native + Socket.IO)
- Devices connect via WebSocket and register their capabilities
- Server sends commands (camera, location, etc.) and receives real results
- Connection tracking and heartbeat monitoring
Provides device hardware access for AI agents:
- Camera capture (snap/clip)
- Screen recording (start/stop)
- Location services
- System notifications
- Command execution
Governance Integration:
- Camera/Location/Notifications: INTERN+ maturity level
- Screen Recording: SUPERVISED+ maturity level
- Command Execution: AUTONOMOUS only (security critical)
- Full audit trail via device_audit table
- Agent execution tracking for all device sessions
Refactored to use standardized decorators and service factory.
"""
import asyncio
from datetime import datetime
import json
from typing import Any, Dict, List, Optional
import uuid
from sqlalchemy.orm import Session
from core.models import DeviceAudit, DeviceNode, DeviceSession
from core.service_factory import ServiceFactory
from core.structured_logger import get_logger
from core.feature_flags import FeatureFlags
logger = get_logger(__name__)
# Import WebSocket communication
try:
from api.device_websocket import (
get_connected_devices_info,
is_device_online,
send_device_command,
)
WEBSOCKET_AVAILABLE = True
logger.info("Device WebSocket module loaded - real device communication enabled")
except ImportError as e:
WEBSOCKET_AVAILABLE = False
logger.warning("Device WebSocket module not available", error=str(e))
logger.warning("Device functions will fail with connection error")
# Security settings
import os
DEVICE_COMMAND_WHITELIST = os.getenv(
"DEVICE_COMMAND_WHITELIST",
"ls,pwd,cat,grep,head,tail,echo,find,ps,top"
).split(",")
DEVICE_SHELL_READ_COMMANDS = ["ls", "pwd", "cat", "grep", "head", "tail", "echo", "date", "whoami"]
DEVICE_SHELL_MONITOR_COMMANDS = ["find", "ps", "top", "df", "du", "netstat", "lsof"]
DEVICE_SCREEN_RECORD_MAX_DURATION = int(os.getenv("DEVICE_SCREEN_RECORD_MAX_DURATION", "3600")) # 1 hour default
# ============================================================================
# Session Manager
# ============================================================================
class DeviceSessionManager:
"""
Manages active device sessions with automatic cleanup.
Sessions are stored in memory and automatically cleaned up after
a timeout period of inactivity.
"""
def __init__(self, session_timeout_minutes: int = 60):
self.sessions: Dict[str, Dict[str, Any]] = {}
self.session_timeout_minutes = session_timeout_minutes
def get_session(self, session_id: str) -> Optional[Dict[str, Any]]:
"""Get an existing session by ID."""
return self.sessions.get(session_id)
def create_session(
self,
user_id: str,
device_node_id: str,
session_type: str,
agent_id: Optional[str] = None,
configuration: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""Create a new device session."""
session_id = str(uuid.uuid4())
session = {
"session_id": session_id,
"user_id": user_id,
"device_node_id": device_node_id,
"session_type": session_type,
"agent_id": agent_id,
"configuration": configuration or {},
"status": "active",
"created_at": datetime.now(),
"last_used": datetime.now()
}
self.sessions[session_id] = session
logger.info(f"Device session {session_id} created (type: {session_type})")
return session
def close_session(self, session_id: str) -> bool:
"""Close a device session."""
if session_id in self.sessions:
session = self.sessions[session_id]
session["status"] = "closed"
session["closed_at"] = datetime.now()
del self.sessions[session_id]
logger.info(f"Device session {session_id} closed")
return True
return False
def cleanup_expired_sessions(self):
"""Remove expired sessions based on timeout."""
now = datetime.now()
expired = []
for session_id, session in self.sessions.items():
last_used = session.get("last_used", session["created_at"])
age_minutes = (now - last_used).total_seconds() / 60
if age_minutes > self.session_timeout_minutes:
expired.append(session_id)
for session_id in expired:
self.close_session(session_id)
logger.info(f"Expired device session {session_id} cleaned up")
return len(expired)
# Singleton instance
_device_session_manager: Optional[DeviceSessionManager] = None
def get_device_session_manager() -> DeviceSessionManager:
"""Get the global device session manager instance."""
global _device_session_manager
if _device_session_manager is None:
_device_session_manager = DeviceSessionManager()
return _device_session_manager
# ============================================================================
# Audit Helper
# ============================================================================
def _create_device_audit(
db: Session,
user_id: str,
device_node_id: str,
action_type: str,
action_params: Dict[str, Any],
success: bool,
result_summary: Optional[str] = None,
error_message: Optional[str] = None,
result_data: Optional[Dict[str, Any]] = None,
file_path: Optional[str] = None,
duration_ms: Optional[int] = None,
agent_id: Optional[str] = None,
agent_execution_id: Optional[str] = None,
session_id: Optional[str] = None,
governance_check_passed: Optional[bool] = None
) -> DeviceAudit:
"""
Create an audit entry for a device action.
Args:
db: Database session
user_id: User who triggered the action
device_node_id: Device that performed the action
action_type: Type of action (e.g., 'camera_snap', 'screen_record_start')
action_params: Parameters passed to the action
success: Whether the action succeeded
result_summary: Human-readable summary of the result
error_message: Error message if action failed
result_data: Structured result data
file_path: Path to any file created (screenshots, recordings)
duration_ms: Duration of the action in milliseconds
agent_id: Agent that performed the action
agent_execution_id: Agent execution record
session_id: Device session ID
governance_check_passed: Whether governance check passed
Returns:
DeviceAudit record
"""
audit = DeviceAudit(
id=str(uuid.uuid4()),
user_id=user_id,
device_node_id=device_node_id,
action_type=action_type,
action_params=action_params,
success=success,
result_summary=result_summary,
error_message=error_message,
result_data=result_data or {},
file_path=file_path,
duration_ms=duration_ms,
agent_id=agent_id,
agent_execution_id=agent_execution_id,
session_id=session_id,
governance_check_passed=governance_check_passed,
created_at=datetime.now()
)
db.add(audit)
db.commit()
logger.info(
f"Device audit created: {action_type} on device {device_node_id} "
f"by user {user_id} - Success: {success}"
)
return audit
# ============================================================================
# Governance Helper
# ============================================================================
async def _check_device_governance(
db: Session,
agent_id: str,
action_type: str,
user_id: str
) -> Dict[str, Any]:
"""
Check if an agent is allowed to perform a device action.
Args:
db: Database session
agent_id: Agent to check
action_type: Action to check (e.g., 'device_camera_snap')
user_id: User requesting the action
Returns:
Governance check result with 'allowed' boolean
"""
if not FeatureFlags.should_enforce_governance('device'):
return {
"allowed": True,
"reason": "Device governance disabled or emergency bypass active",
"governance_check_passed": True
}
try:
governance = ServiceFactory.get_governance_service(db)
check = governance.can_perform_action(agent_id, action_type)
return {
"allowed": check["allowed"],
"reason": check["reason"],
"governance_check_passed": check["allowed"]
}
except Exception as e:
logger.error(f"Governance check failed for {action_type}: {e}")
# Fail open for availability
return {
"allowed": True,
"reason": f"Governance check failed: {str(e)}",
"governance_check_passed": False
}
# ============================================================================
# Device Functions
# ============================================================================
async def device_camera_snap(
db: Session,
user_id: str,
device_node_id: str,
agent_id: Optional[str] = None,
camera_id: Optional[str] = None,
resolution: Optional[str] = "1920x1080",
save_path: Optional[str] = None
) -> Dict[str, Any]:
"""
Capture an image from the device camera.
Action Complexity: 2 (INTERN+)
Args:
db: Database session
user_id: User requesting the capture
device_node_id: Device to capture from
agent_id: Agent performing the action (for governance)
camera_id: Specific camera to use (if multiple)
resolution: Image resolution (e.g., "1920x1080")
save_path: Where to save the image
Returns:
Dict with success status and file path/base64 data
"""
start_time = datetime.now()
governance_check = None
# Governance check
if agent_id:
governance_check = await _check_device_governance(
db, agent_id, "device_camera_snap", user_id
)
if not governance_check["allowed"]:
return {
"success": False,
"error": governance_check["reason"],
"governance_blocked": True
}
try:
# Check WebSocket availability
if not WEBSOCKET_AVAILABLE:
raise ValueError("Device WebSocket module not available. Cannot communicate with devices.")
# Check if device is online
if not is_device_online(device_node_id):
raise ValueError(
f"Device {device_node_id} is not currently connected. "
"Please ensure the mobile app is running and connected via WebSocket."
)
# Send WebSocket command to device
response = await send_device_command(
device_node_id=device_node_id,
command="camera_snap",
params={
"camera_id": camera_id or "default",
"resolution": resolution,
"save_path": save_path
},
db=db
)
if not response.get("success"):
raise ValueError(response.get("error", "Camera capture failed on device"))
result = {
"success": True,
"file_path": response.get("file_path"),
"base64_data": response.get("data", {}).get("base64_data"),
"resolution": resolution,
"camera_id": camera_id or "default",
"captured_at": datetime.now().isoformat()
}
# Create audit entry
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="camera_snap",
action_params={
"camera_id": camera_id,
"resolution": resolution,
"save_path": save_path
},
success=True,
result_summary=f"Camera capture successful: {result['file_path']}",
result_data=result,
file_path=result["file_path"],
duration_ms=duration_ms,
agent_id=agent_id,
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.info(f"Camera snap successful for device {device_node_id}")
return result
except Exception as e:
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
error_msg = f"Camera snap failed: {str(e)}"
# Create audit entry for failure
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="camera_snap",
action_params={
"camera_id": camera_id,
"resolution": resolution,
"save_path": save_path
},
success=False,
error_message=error_msg,
duration_ms=duration_ms,
agent_id=agent_id,
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.error(error_msg)
return {
"success": False,
"error": error_msg
}
async def device_screen_record_start(
db: Session,
user_id: str,
device_node_id: str,
agent_id: Optional[str] = None,
duration_seconds: Optional[int] = None,
audio_enabled: bool = False,
resolution: Optional[str] = "1920x1080",
output_format: str = "mp4"
) -> Dict[str, Any]:
"""
Start a screen recording session.
Action Complexity: 3 (SUPERVISED+)
Args:
db: Database session
user_id: User requesting the recording
device_node_id: Device to record
agent_id: Agent performing the action (for governance)
duration_seconds: Maximum duration (default: from env, 3600s)
audio_enabled: Whether to capture audio
resolution: Recording resolution
output_format: Output format (mp4, webm, gif)
Returns:
Dict with session_id and recording details
"""
start_time = datetime.now()
governance_check = None
# Governance check
if agent_id:
governance_check = await _check_device_governance(
db, agent_id, "device_screen_record_start", user_id
)
if not governance_check["allowed"]:
return {
"success": False,
"error": governance_check["reason"],
"governance_blocked": True
}
try:
# Get device node
device = db.query(DeviceNode).filter(
DeviceNode.device_id == device_node_id
).first()
if not device:
raise ValueError(f"Device {device_node_id} not found")
# Validate duration
max_duration = DEVICE_SCREEN_RECORD_MAX_DURATION
if duration_seconds and duration_seconds > max_duration:
raise ValueError(
f"Duration {duration_seconds}s exceeds maximum {max_duration}s"
)
# Create session
session_manager = get_device_session_manager()
session = session_manager.create_session(
user_id=user_id,
device_node_id=device_node_id,
session_type="screen_record",
agent_id=agent_id,
configuration={
"duration_seconds": duration_seconds or max_duration,
"audio_enabled": audio_enabled,
"resolution": resolution,
"output_format": output_format
}
)
# Create database session record
db_session = DeviceSession(
id=str(uuid.uuid4()),
session_id=session["session_id"],
device_node_id=device_node_id,
user_id=user_id,
agent_id=agent_id,
session_type="screen_record",
status="active",
configuration=session["configuration"],
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
db.add(db_session)
db.commit()
# Send WebSocket command to device to start recording
if WEBSOCKET_AVAILABLE:
try:
response = await send_device_command(
device_node_id=device_node_id,
command="screen_record_start",
params={
"session_id": session["session_id"],
"duration_seconds": duration_seconds or max_duration,
"audio_enabled": audio_enabled,
"resolution": resolution,
"output_format": output_format
},
db=db
)
if not response.get("success"):
# Update session status to failed
db_session.status = "failed"
db.commit()
raise ValueError(response.get("error", "Screen recording start failed on device"))
except ValueError as e:
# Update session status to failed
db_session.status = "failed"
db.commit()
raise
result = {
"success": True,
"session_id": session["session_id"],
"device_node_id": device_node_id,
"configuration": session["configuration"],
"started_at": session["created_at"].isoformat()
}
# Create audit entry
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="screen_record_start",
action_params=session["configuration"],
success=True,
result_summary=f"Screen recording started: {session['session_id']}",
result_data=result,
duration_ms=duration_ms,
agent_id=agent_id,
session_id=session["session_id"],
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.info(f"Screen recording started for device {device_node_id}: {session['session_id']}")
return result
except Exception as e:
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
error_msg = f"Screen record start failed: {str(e)}"
# Create audit entry for failure
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="screen_record_start",
action_params={
"duration_seconds": duration_seconds,
"audio_enabled": audio_enabled,
"resolution": resolution,
"output_format": output_format
},
success=False,
error_message=error_msg,
duration_ms=duration_ms,
agent_id=agent_id,
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.error(error_msg)
return {
"success": False,
"error": error_msg
}
async def device_screen_record_stop(
db: Session,
user_id: str,
session_id: str
) -> Dict[str, Any]:
"""
Stop a screen recording session.
Action Complexity: 3 (SUPERVISED+)
Args:
db: Database session
user_id: User requesting to stop
session_id: Recording session to stop
Returns:
Dict with file path and recording details
"""
start_time = datetime.now()
try:
# Get session
session_manager = get_device_session_manager()
session = session_manager.get_session(session_id)
if not session:
raise ValueError(f"Session {session_id} not found")
if session["user_id"] != user_id:
raise ValueError(f"Session {session_id} does not belong to user {user_id}")
# Send WebSocket command to device to stop recording
file_path = None
duration_seconds = 0
if WEBSOCKET_AVAILABLE:
try:
response = await send_device_command(
device_node_id=session["device_node_id"],
command="screen_record_stop",
params={
"session_id": session_id
},
db=db
)
if response.get("success"):
file_path = response.get("file_path")
duration_seconds = response.get("data", {}).get("duration_seconds", 0)
else:
raise ValueError(response.get("error", "Screen recording stop failed on device"))
except ValueError as e:
logger.error(f"Failed to stop screen recording: {e}")
# Continue to close session even if stop command fails
# Close session
session_manager.close_session(session_id)
# Update database session record
db_session = db.query(DeviceSession).filter(
DeviceSession.session_id == session_id
).first()
if db_session:
db_session.status = "closed"
db_session.closed_at = datetime.now()
db.commit()
result = {
"success": True,
"session_id": session_id,
"file_path": file_path or f"/tmp/recording_{session_id}.mp4",
"duration_seconds": duration_seconds or (datetime.now() - session["created_at"]).total_seconds(),
"stopped_at": datetime.now().isoformat()
}
# Create audit entry
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=session["device_node_id"],
action_type="screen_record_stop",
action_params={"session_id": session_id},
success=True,
result_summary=f"Screen recording stopped: {result['file_path']}",
result_data=result,
file_path=result["file_path"],
duration_ms=duration_ms,
agent_id=session.get("agent_id"),
session_id=session_id
)
logger.info(f"Screen recording stopped: {session_id}")
return result
except Exception as e:
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
error_msg = f"Screen record stop failed: {str(e)}"
logger.error(error_msg)
return {
"success": False,
"error": error_msg
}
async def device_get_location(
db: Session,
user_id: str,
device_node_id: str,
agent_id: Optional[str] = None,
accuracy: str = "high"
) -> Dict[str, Any]:
"""
Get the device's current location.
Action Complexity: 2 (INTERN+)
Args:
db: Database session
user_id: User requesting location
device_node_id: Device to locate
agent_id: Agent performing the action (for governance)
accuracy: Location accuracy (high, medium, low)
Returns:
Dict with latitude, longitude, and accuracy
"""
start_time = datetime.now()
governance_check = None
# Governance check
if agent_id:
governance_check = await _check_device_governance(
db, agent_id, "device_get_location", user_id
)
if not governance_check["allowed"]:
return {
"success": False,
"error": governance_check["reason"],
"governance_blocked": True
}
try:
# Check WebSocket availability
if not WEBSOCKET_AVAILABLE:
raise ValueError("Device WebSocket module not available. Cannot communicate with devices.")
# Check if device is online
if not is_device_online(device_node_id):
raise ValueError(
f"Device {device_node_id} is not currently connected. "
"Please ensure the mobile app is running and connected via WebSocket."
)
# Send WebSocket command to device
response = await send_device_command(
device_node_id=device_node_id,
command="get_location",
params={
"accuracy": accuracy
},
db=db
)
if not response.get("success"):
raise ValueError(response.get("error", "Get location failed on device"))
location_data = response.get("data", {})
result = {
"success": True,
"latitude": location_data.get("latitude"),
"longitude": location_data.get("longitude"),
"accuracy": accuracy,
"altitude": location_data.get("altitude"),
"timestamp": location_data.get("timestamp", datetime.now().isoformat())
}
# Create audit entry
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="get_location",
action_params={"accuracy": accuracy},
success=True,
result_summary=f"Location retrieved: {result['latitude']}, {result['longitude']}",
result_data=result,
duration_ms=duration_ms,
agent_id=agent_id,
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.info(f"Location retrieved for device {device_node_id}")
return result
except Exception as e:
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
error_msg = f"Get location failed: {str(e)}"
# Create audit entry for failure
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="get_location",
action_params={"accuracy": accuracy},
success=False,
error_message=error_msg,
duration_ms=duration_ms,
agent_id=agent_id,
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.error(error_msg)
return {
"success": False,
"error": error_msg
}
async def device_send_notification(
db: Session,
user_id: str,
device_node_id: str,
title: str,
body: str,
agent_id: Optional[str] = None,
icon: Optional[str] = None,
sound: Optional[str] = None
) -> Dict[str, Any]:
"""
Send a system notification to the device.
Action Complexity: 2 (INTERN+)
Args:
db: Database session
user_id: User sending notification
device_node_id: Device to notify
title: Notification title
body: Notification body
agent_id: Agent performing the action (for governance)
icon: Optional icon path
sound: Optional sound to play
Returns:
Dict with success status
"""
start_time = datetime.now()
governance_check = None
# Governance check
if agent_id:
governance_check = await _check_device_governance(
db, agent_id, "device_send_notification", user_id
)
if not governance_check["allowed"]:
return {
"success": False,
"error": governance_check["reason"],
"governance_blocked": True
}
try:
# Check WebSocket availability
if not WEBSOCKET_AVAILABLE:
raise ValueError("Device WebSocket module not available. Cannot communicate with devices.")
# Check if device is online
if not is_device_online(device_node_id):
raise ValueError(
f"Device {device_node_id} is not currently connected. "
"Please ensure the mobile app is running and connected via WebSocket."
)
# Send WebSocket command to device
response = await send_device_command(
device_node_id=device_node_id,
command="send_notification",
params={
"title": title,
"body": body,
"icon": icon,
"sound": sound
},
db=db
)
if not response.get("success"):
raise ValueError(response.get("error", "Send notification failed on device"))
result = {
"success": True,
"device_node_id": device_node_id,
"title": title,
"body": body,
"sent_at": datetime.now().isoformat()
}
# Create audit entry
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="send_notification",
action_params={
"title": title,
"body": body,
"icon": icon,
"sound": sound
},
success=True,
result_summary=f"Notification sent: {title}",
result_data=result,
duration_ms=duration_ms,
agent_id=agent_id,
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.info(f"Notification sent to device {device_node_id}: {title}")
return result
except Exception as e:
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
error_msg = f"Send notification failed: {str(e)}"
# Create audit entry for failure
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="send_notification",
action_params={
"title": title,
"body": body,
"icon": icon,
"sound": sound
},
success=False,
error_message=error_msg,
duration_ms=duration_ms,
agent_id=agent_id,
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.error(error_msg)
return {
"success": False,
"error": error_msg
}
async def device_execute_command(
db: Session,
user_id: str,
device_node_id: str,
command: str,
agent_id: Optional[str] = None,
working_dir: Optional[str] = None,
timeout_seconds: int = 30,
environment: Optional[Dict[str, str]] = None
) -> Dict[str, Any]:
"""
Execute a shell command on the device.
Action Complexity: 4 (AUTONOMOUS only)
SECURITY CRITICAL:
- AUTONOMOUS agents only
- Command whitelist enforced
- Timeout enforced
- Working directory restricted
- No interactive shells
Args:
db: Database session
user_id: User requesting execution
device_node_id: Device to execute on
command: Command to execute
agent_id: Agent performing the action (for governance)
working_dir: Working directory for command
timeout_seconds: Command timeout (default: 30s)
environment: Environment variables
Returns:
Dict with exit code, stdout, stderr
"""
start_time = datetime.now()
governance_check = None
# Governance check
if agent_id:
# Determine command category for graduated access
command_base = command.split()[0] if command.strip() else ""
governance_action = "device_execute_command" # Default (Complexity 4)
if command_base in DEVICE_SHELL_READ_COMMANDS:
governance_action = "device_shell_read" # Complexity 2
elif command_base in DEVICE_SHELL_MONITOR_COMMANDS:
governance_action = "device_shell_monitor" # Complexity 3
governance_check = await _check_device_governance(
db, agent_id, governance_action, user_id
)
if not governance_check["allowed"]:
return {
"success": False,
"error": governance_check["reason"],
"governance_blocked": True
}
try:
# Get device node
device = db.query(DeviceNode).filter(
DeviceNode.device_id == device_node_id
).first()
if not device:
raise ValueError(f"Device {device_node_id} not found")
# Security: Validate command against whitelist
command_base = command.split()[0] if command.strip() else ""
if command_base not in DEVICE_COMMAND_WHITELIST:
raise ValueError(
f"Command '{command_base}' not in whitelist. "
f"Allowed: {DEVICE_COMMAND_WHITELIST}"
)
# Security: Enforce timeout
if timeout_seconds > 300: # 5 minutes max
raise ValueError(f"Timeout {timeout_seconds}s exceeds maximum 300s")
# Check WebSocket availability
if not WEBSOCKET_AVAILABLE:
raise ValueError("Device WebSocket module not available. Cannot communicate with devices.")
# Check if device is online
if not is_device_online(device_node_id):
raise ValueError(
f"Device {device_node_id} is not currently connected. "
"Please ensure the mobile app is running and connected via WebSocket."
)
# Send WebSocket command to device
response = await send_device_command(
device_node_id=device_node_id,
command="execute_command",
params={
"command": command,
"working_dir": working_dir,
"timeout_seconds": timeout_seconds,
"environment": environment
},
db=db
)
if not response.get("success"):
raise ValueError(response.get("error", "Command execution failed on device"))
command_data = response.get("data", {})
result = {
"success": True,
"exit_code": command_data.get("exit_code", 0),
"stdout": command_data.get("stdout", ""),
"stderr": command_data.get("stderr", ""),
"command": command,
"working_dir": working_dir,
"timeout_seconds": timeout_seconds,
"executed_at": datetime.now().isoformat()
}
# Create audit entry
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="execute_command",
action_params={
"command": command,
"working_dir": working_dir,
"timeout_seconds": timeout_seconds,
"environment": environment
},
success=True,
result_summary=f"Command executed: {command} (exit: {result['exit_code']})",
result_data=result,
duration_ms=duration_ms,
agent_id=agent_id,
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.info(f"Command executed on device {device_node_id}: {command}")
return result
except Exception as e:
duration_ms = int((datetime.now() - start_time).total_seconds() * 1000)
error_msg = f"Execute command failed: {str(e)}"
# Create audit entry for failure
_create_device_audit(
db=db,
user_id=user_id,
device_node_id=device_node_id,
action_type="execute_command",
action_params={
"command": command,
"working_dir": working_dir,
"timeout_seconds": timeout_seconds,
"environment": environment
},
success=False,
error_message=error_msg,
duration_ms=duration_ms,
agent_id=agent_id,
governance_check_passed=governance_check["governance_check_passed"] if governance_check else None
)
logger.error(error_msg)
return {
"success": False,
"error": error_msg
}
# ============================================================================
# Helper Functions
# ============================================================================
async def get_device_info(
db: Session,
device_node_id: str
) -> Optional[Dict[str, Any]]:
"""
Get information about a device.
Args:
db: Database session
device_node_id: Device ID
Returns:
Device information or None
"""
device = db.query(DeviceNode).filter(
DeviceNode.device_id == device_node_id
).first()
if not device:
return None
return {
"id": device.id,
"device_id": device.device_id,
"name": device.name,
"node_type": device.node_type,
"status": device.status,
"platform": device.platform,
"platform_version": device.platform_version,
"architecture": device.architecture,
"capabilities": device.capabilities,
"capabilities_detailed": device.capabilities_detailed,
"hardware_info": device.hardware_info,
"last_seen": device.last_seen.isoformat() if device.last_seen else None
}
async def list_devices(
db: Session,
user_id: str,
status: Optional[str] = None
) -> List[Dict[str, Any]]:
"""
List devices available to a user.
Args:
db: Database session
user_id: User ID
status: Filter by status (online, offline, busy)
Returns:
List of device information
"""
query = db.query(DeviceNode).filter(DeviceNode.user_id == user_id)
if status:
query = query.filter(DeviceNode.status == status)
devices = query.all()
return [
{
"id": device.id,
"device_id": device.device_id,
"name": device.name,
"node_type": device.node_type,
"status": device.status,
"platform": device.platform,
"capabilities": device.capabilities,
"last_seen": device.last_seen.isoformat() if device.last_seen else None
}
for device in devices
]
async def execute_device_command(
db: Session,
user_id: str,
agent_id: Optional[str],
device_id: str,
command_type: str,
parameters: Dict[str, Any],
execution_id: Optional[str] = None
) -> Dict[str, Any]:
"""
Generic wrapper for executing device commands.
Routes to the appropriate specialized device function based on command_type.
This provides a unified interface for the proposal service and other components.
Args:
db: Database session
user_id: User ID requesting the command
agent_id: Optional agent ID for governance
device_id: Device ID to execute command on
command_type: Type of command (camera, location, notification, command)
parameters: Command parameters (varies by command_type)
execution_id: Optional execution ID for tracking
Returns:
Dict with success status and command-specific data
Command Types and Parameters:
- camera: {"timeout": int}
- location: {"high_accuracy": bool}
- notification: {"title": str, "body": str}
- command: {"command": str, "working_dir": str, "timeout": int}
"""
try:
if command_type == "camera":
# Camera capture
timeout = parameters.get("timeout", 10)
return await device_camera_snap(
db=db,
user_id=user_id,
agent_id=agent_id,
timeout_seconds=timeout
)
elif command_type == "location":
# Get location
high_accuracy = parameters.get("high_accuracy", True)
return await device_get_location(
db=db,
user_id=user_id,
agent_id=agent_id,
high_accuracy=high_accuracy
)
elif command_type == "notification":
# Send notification
title = parameters.get("title", "Notification")
body = parameters.get("body", "")
return await device_send_notification(
db=db,
user_id=user_id,
agent_id=agent_id,
title=title,
body=body
)
elif command_type == "command":
# Execute shell command
command = parameters.get("command")
working_dir = parameters.get("working_dir")
timeout = parameters.get("timeout", 30)
return await device_execute_command(
db=db,
user_id=user_id,
device_node_id=device_id,
command=command,
agent_id=agent_id,
working_dir=working_dir,
timeout_seconds=timeout
)
else:
logger.warning(f"Unknown device command type: {command_type}")
return {
"success": False,
"error": f"Unknown command type: {command_type}. Supported types: camera, location, notification, command"
}
except Exception as e:
logger.error(f"Failed to execute device command: {e}")
return {"success": False, "error": str(e)}