cua-computer 0.2.2__tar.gz → 0.2.4__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. {cua_computer-0.2.2 → cua_computer-0.2.4}/PKG-INFO +1 -1
  2. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/computer.py +29 -10
  3. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/interface/base.py +5 -1
  4. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/interface/factory.py +8 -4
  5. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/interface/linux.py +31 -3
  6. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/interface/macos.py +33 -5
  7. cua_computer-0.2.4/computer/providers/cloud/provider.py +75 -0
  8. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/providers/factory.py +4 -6
  9. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/providers/lumier/provider.py +7 -1
  10. {cua_computer-0.2.2 → cua_computer-0.2.4}/pyproject.toml +3 -3
  11. cua_computer-0.2.2/computer/providers/cloud/provider.py +0 -100
  12. {cua_computer-0.2.2 → cua_computer-0.2.4}/README.md +0 -0
  13. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/__init__.py +0 -0
  14. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/interface/__init__.py +0 -0
  15. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/interface/models.py +0 -0
  16. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/logger.py +0 -0
  17. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/models.py +0 -0
  18. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/providers/__init__.py +0 -0
  19. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/providers/base.py +0 -0
  20. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/providers/cloud/__init__.py +0 -0
  21. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/providers/lume/__init__.py +0 -0
  22. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/providers/lume/provider.py +0 -0
  23. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/providers/lume_api.py +0 -0
  24. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/providers/lumier/__init__.py +0 -0
  25. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/telemetry.py +0 -0
  26. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/ui/__init__.py +0 -0
  27. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/ui/gradio/__init__.py +0 -0
  28. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/ui/gradio/app.py +0 -0
  29. {cua_computer-0.2.2 → cua_computer-0.2.4}/computer/utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cua-computer
3
- Version: 0.2.2
3
+ Version: 0.2.4
4
4
  Summary: Computer-Use Interface (CUI) framework powering Cua
5
5
  Author-Email: TryCua <gh@trycua.com>
6
6
  Requires-Python: >=3.10
@@ -38,7 +38,8 @@ class Computer:
38
38
  noVNC_port: Optional[int] = 8006,
39
39
  host: str = os.environ.get("PYLUME_HOST", "localhost"),
40
40
  storage: Optional[str] = None,
41
- ephemeral: bool = False
41
+ ephemeral: bool = False,
42
+ api_key: Optional[str] = None
42
43
  ):
43
44
  """Initialize a new Computer instance.
44
45
 
@@ -77,6 +78,8 @@ class Computer:
77
78
  self.os_type = os_type
78
79
  self.provider_type = provider_type
79
80
  self.ephemeral = ephemeral
81
+
82
+ self.api_key = api_key
80
83
 
81
84
  # The default is currently to use non-ephemeral storage
82
85
  if storage and ephemeral and storage != "ephemeral":
@@ -256,9 +259,7 @@ class Computer:
256
259
  elif self.provider_type == VMProviderType.CLOUD:
257
260
  self.config.vm_provider = VMProviderFactory.create_provider(
258
261
  self.provider_type,
259
- port=port,
260
- host=host,
261
- storage=storage,
262
+ api_key=self.api_key,
262
263
  verbose=verbose,
263
264
  )
264
265
  else:
@@ -392,12 +393,25 @@ class Computer:
392
393
  self.logger.info(f"Initializing interface for {self.os_type} at {ip_address}")
393
394
  from .interface.base import BaseComputerInterface
394
395
 
395
- self._interface = cast(
396
- BaseComputerInterface,
397
- InterfaceFactory.create_interface_for_os(
398
- os=self.os_type, ip_address=ip_address # type: ignore[arg-type]
399
- ),
400
- )
396
+ # Pass authentication credentials if using cloud provider
397
+ if self.provider_type == VMProviderType.CLOUD and self.api_key and self.config.name:
398
+ self._interface = cast(
399
+ BaseComputerInterface,
400
+ InterfaceFactory.create_interface_for_os(
401
+ os=self.os_type,
402
+ ip_address=ip_address,
403
+ api_key=self.api_key,
404
+ vm_name=self.config.name
405
+ ),
406
+ )
407
+ else:
408
+ self._interface = cast(
409
+ BaseComputerInterface,
410
+ InterfaceFactory.create_interface_for_os(
411
+ os=self.os_type,
412
+ ip_address=ip_address
413
+ ),
414
+ )
401
415
 
402
416
  # Wait for the WebSocket interface to be ready
403
417
  self.logger.info("Connecting to WebSocket interface...")
@@ -492,6 +506,11 @@ class Computer:
492
506
 
493
507
  # Call the provider's get_ip method which will wait indefinitely
494
508
  storage_param = "ephemeral" if self.ephemeral else self.storage
509
+
510
+ # Log the image being used
511
+ self.logger.info(f"Running VM using image: {self.image}")
512
+
513
+ # Call provider.get_ip with explicit image parameter
495
514
  ip = await self.config.vm_provider.get_ip(
496
515
  name=self.config.name,
497
516
  storage=storage_param,
@@ -8,17 +8,21 @@ from ..logger import Logger, LogLevel
8
8
  class BaseComputerInterface(ABC):
9
9
  """Base class for computer control interfaces."""
10
10
 
11
- def __init__(self, ip_address: str, username: str = "lume", password: str = "lume"):
11
+ def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
12
12
  """Initialize interface.
13
13
 
14
14
  Args:
15
15
  ip_address: IP address of the computer to control
16
16
  username: Username for authentication
17
17
  password: Password for authentication
18
+ api_key: Optional API key for cloud authentication
19
+ vm_name: Optional VM name for cloud authentication
18
20
  """
19
21
  self.ip_address = ip_address
20
22
  self.username = username
21
23
  self.password = password
24
+ self.api_key = api_key
25
+ self.vm_name = vm_name
22
26
  self.logger = Logger("cua.interface", LogLevel.NORMAL)
23
27
 
24
28
  @abstractmethod
@@ -1,6 +1,6 @@
1
1
  """Factory for creating computer interfaces."""
2
2
 
3
- from typing import Literal
3
+ from typing import Literal, Optional
4
4
  from .base import BaseComputerInterface
5
5
 
6
6
  class InterfaceFactory:
@@ -9,13 +9,17 @@ class InterfaceFactory:
9
9
  @staticmethod
10
10
  def create_interface_for_os(
11
11
  os: Literal['macos', 'linux'],
12
- ip_address: str
12
+ ip_address: str,
13
+ api_key: Optional[str] = None,
14
+ vm_name: Optional[str] = None
13
15
  ) -> BaseComputerInterface:
14
16
  """Create an interface for the specified OS.
15
17
 
16
18
  Args:
17
19
  os: Operating system type ('macos' or 'linux')
18
20
  ip_address: IP address of the computer to control
21
+ api_key: Optional API key for cloud authentication
22
+ vm_name: Optional VM name for cloud authentication
19
23
 
20
24
  Returns:
21
25
  BaseComputerInterface: The appropriate interface for the OS
@@ -28,8 +32,8 @@ class InterfaceFactory:
28
32
  from .linux import LinuxComputerInterface
29
33
 
30
34
  if os == 'macos':
31
- return MacOSComputerInterface(ip_address)
35
+ return MacOSComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
32
36
  elif os == 'linux':
33
- return LinuxComputerInterface(ip_address)
37
+ return LinuxComputerInterface(ip_address, api_key=api_key, vm_name=vm_name)
34
38
  else:
35
39
  raise ValueError(f"Unsupported OS type: {os}")
@@ -15,8 +15,8 @@ from .models import Key, KeyType
15
15
  class LinuxComputerInterface(BaseComputerInterface):
16
16
  """Interface for Linux."""
17
17
 
18
- def __init__(self, ip_address: str, username: str = "lume", password: str = "lume"):
19
- super().__init__(ip_address, username, password)
18
+ def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
19
+ super().__init__(ip_address, username, password, api_key, vm_name)
20
20
  self._ws = None
21
21
  self._reconnect_task = None
22
22
  self._closed = False
@@ -37,7 +37,9 @@ class LinuxComputerInterface(BaseComputerInterface):
37
37
  Returns:
38
38
  WebSocket URI for the Computer API Server
39
39
  """
40
- return f"ws://{self.ip_address}:8000/ws"
40
+ protocol = "wss" if self.api_key else "ws"
41
+ port = "8443" if self.api_key else "8000"
42
+ return f"{protocol}://{self.ip_address}:{port}/ws"
41
43
 
42
44
  async def _keep_alive(self):
43
45
  """Keep the WebSocket connection alive with automatic reconnection."""
@@ -86,6 +88,32 @@ class LinuxComputerInterface(BaseComputerInterface):
86
88
  timeout=30,
87
89
  )
88
90
  self.logger.info("WebSocket connection established")
91
+
92
+ # If api_key and vm_name are provided, perform authentication handshake
93
+ if self.api_key and self.vm_name:
94
+ self.logger.info("Performing authentication handshake...")
95
+ auth_message = {
96
+ "command": "authenticate",
97
+ "params": {
98
+ "api_key": self.api_key,
99
+ "container_name": self.vm_name
100
+ }
101
+ }
102
+ await self._ws.send(json.dumps(auth_message))
103
+
104
+ # Wait for authentication response
105
+ auth_response = await asyncio.wait_for(self._ws.recv(), timeout=10)
106
+ auth_result = json.loads(auth_response)
107
+
108
+ if not auth_result.get("success"):
109
+ error_msg = auth_result.get("error", "Authentication failed")
110
+ self.logger.error(f"Authentication failed: {error_msg}")
111
+ await self._ws.close()
112
+ self._ws = None
113
+ raise ConnectionError(f"Authentication failed: {error_msg}")
114
+
115
+ self.logger.info("Authentication successful")
116
+
89
117
  self._reconnect_delay = 1 # Reset reconnect delay on successful connection
90
118
  self._last_ping = time.time()
91
119
  retry_count = 0 # Reset retry count on successful connection
@@ -13,10 +13,10 @@ from .models import Key, KeyType
13
13
 
14
14
 
15
15
  class MacOSComputerInterface(BaseComputerInterface):
16
- """Interface for MacOS."""
16
+ """Interface for macOS."""
17
17
 
18
- def __init__(self, ip_address: str, username: str = "lume", password: str = "lume"):
19
- super().__init__(ip_address, username, password)
18
+ def __init__(self, ip_address: str, username: str = "lume", password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None):
19
+ super().__init__(ip_address, username, password, api_key, vm_name)
20
20
  self._ws = None
21
21
  self._reconnect_task = None
22
22
  self._closed = False
@@ -27,7 +27,7 @@ class MacOSComputerInterface(BaseComputerInterface):
27
27
  self._max_reconnect_delay = 30 # Maximum delay between reconnection attempts
28
28
  self._log_connection_attempts = True # Flag to control connection attempt logging
29
29
 
30
- # Set logger name for MacOS interface
30
+ # Set logger name for macOS interface
31
31
  self.logger = Logger("cua.interface.macos", LogLevel.NORMAL)
32
32
 
33
33
  @property
@@ -37,7 +37,9 @@ class MacOSComputerInterface(BaseComputerInterface):
37
37
  Returns:
38
38
  WebSocket URI for the Computer API Server
39
39
  """
40
- return f"ws://{self.ip_address}:8000/ws"
40
+ protocol = "wss" if self.api_key else "ws"
41
+ port = "8443" if self.api_key else "8000"
42
+ return f"{protocol}://{self.ip_address}:{port}/ws"
41
43
 
42
44
  async def _keep_alive(self):
43
45
  """Keep the WebSocket connection alive with automatic reconnection."""
@@ -86,6 +88,32 @@ class MacOSComputerInterface(BaseComputerInterface):
86
88
  timeout=30,
87
89
  )
88
90
  self.logger.info("WebSocket connection established")
91
+
92
+ # If api_key and vm_name are provided, perform authentication handshake
93
+ if self.api_key and self.vm_name:
94
+ self.logger.info("Performing authentication handshake...")
95
+ auth_message = {
96
+ "command": "authenticate",
97
+ "params": {
98
+ "api_key": self.api_key,
99
+ "container_name": self.vm_name
100
+ }
101
+ }
102
+ await self._ws.send(json.dumps(auth_message))
103
+
104
+ # Wait for authentication response
105
+ auth_response = await asyncio.wait_for(self._ws.recv(), timeout=10)
106
+ auth_result = json.loads(auth_response)
107
+
108
+ if not auth_result.get("success"):
109
+ error_msg = auth_result.get("error", "Authentication failed")
110
+ self.logger.error(f"Authentication failed: {error_msg}")
111
+ await self._ws.close()
112
+ self._ws = None
113
+ raise ConnectionError(f"Authentication failed: {error_msg}")
114
+
115
+ self.logger.info("Authentication successful")
116
+
89
117
  self._reconnect_delay = 1 # Reset reconnect delay on successful connection
90
118
  self._last_ping = time.time()
91
119
  retry_count = 0 # Reset retry count on successful connection
@@ -0,0 +1,75 @@
1
+ """Cloud VM provider implementation.
2
+
3
+ This module contains a stub implementation for a future cloud VM provider.
4
+ """
5
+
6
+ import logging
7
+ from typing import Dict, List, Optional, Any
8
+
9
+ from ..base import BaseVMProvider, VMProviderType
10
+
11
+ # Setup logging
12
+ logger = logging.getLogger(__name__)
13
+
14
+ import asyncio
15
+ import aiohttp
16
+ from urllib.parse import urlparse
17
+
18
+ class CloudProvider(BaseVMProvider):
19
+ """Cloud VM Provider implementation."""
20
+ def __init__(
21
+ self,
22
+ api_key: str,
23
+ verbose: bool = False,
24
+ **kwargs,
25
+ ):
26
+ """
27
+ Args:
28
+ api_key: API key for authentication
29
+ name: Name of the VM
30
+ verbose: Enable verbose logging
31
+ """
32
+ assert api_key, "api_key required for CloudProvider"
33
+ self.api_key = api_key
34
+ self.verbose = verbose
35
+
36
+ @property
37
+ def provider_type(self) -> VMProviderType:
38
+ return VMProviderType.CLOUD
39
+
40
+ async def __aenter__(self):
41
+ return self
42
+
43
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
44
+ pass
45
+
46
+ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
47
+ """Get VM VNC URL by name using the cloud API."""
48
+ return {"name": name, "hostname": f"{name}.containers.cloud.trycua.com"}
49
+
50
+ async def list_vms(self) -> List[Dict[str, Any]]:
51
+ logger.warning("CloudProvider.list_vms is not implemented")
52
+ return []
53
+
54
+ async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
55
+ logger.warning("CloudProvider.run_vm is not implemented")
56
+ return {"name": name, "status": "unavailable", "message": "CloudProvider is not implemented"}
57
+
58
+ async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
59
+ logger.warning("CloudProvider.stop_vm is not implemented")
60
+ return {"name": name, "status": "stopped", "message": "CloudProvider is not implemented"}
61
+
62
+ async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
63
+ logger.warning("CloudProvider.update_vm is not implemented")
64
+ return {"name": name, "status": "unchanged", "message": "CloudProvider is not implemented"}
65
+
66
+ async def get_ip(self, name: Optional[str] = None, storage: Optional[str] = None, retry_delay: int = 2) -> str:
67
+ """
68
+ Return the VM's IP address as '{container_name}.containers.cloud.trycua.com'.
69
+ Uses the provided 'name' argument (the VM name requested by the caller),
70
+ falling back to self.name only if 'name' is None.
71
+ Retries up to 3 times with retry_delay seconds if hostname is not available.
72
+ """
73
+ if name is None:
74
+ raise ValueError("VM name is required for CloudProvider.get_ip")
75
+ return f"{name}.containers.cloud.trycua.com"
@@ -22,7 +22,8 @@ class VMProviderFactory:
22
22
  image: Optional[str] = None,
23
23
  verbose: bool = False,
24
24
  ephemeral: bool = False,
25
- noVNC_port: Optional[int] = None
25
+ noVNC_port: Optional[int] = None,
26
+ **kwargs,
26
27
  ) -> BaseVMProvider:
27
28
  """Create a VM provider of the specified type.
28
29
 
@@ -101,12 +102,9 @@ class VMProviderFactory:
101
102
  elif provider_type == VMProviderType.CLOUD:
102
103
  try:
103
104
  from .cloud import CloudProvider
104
- # Return the stub implementation of CloudProvider
105
105
  return CloudProvider(
106
- host=host,
107
- port=port,
108
- storage=storage,
109
- verbose=verbose
106
+ verbose=verbose,
107
+ **kwargs,
110
108
  )
111
109
  except ImportError as e:
112
110
  logger.error(f"Failed to import CloudProvider: {e}")
@@ -344,9 +344,15 @@ class LumierProvider(BaseVMProvider):
344
344
  # Use the VM image passed from the Computer class
345
345
  print(f"Using VM image: {self.image}")
346
346
 
347
+ # If ghcr.io is in the image, use the full image name
348
+ if "ghcr.io" in self.image:
349
+ vm_image = self.image
350
+ else:
351
+ vm_image = f"ghcr.io/trycua/{self.image}"
352
+
347
353
  cmd.extend([
348
354
  "-e", f"VM_NAME={self.container_name}",
349
- "-e", f"VERSION=ghcr.io/trycua/{self.image}",
355
+ "-e", f"VERSION={vm_image}",
350
356
  "-e", f"CPU_CORES={run_opts.get('cpu', '4')}",
351
357
  "-e", f"RAM_SIZE={memory_mb}",
352
358
  ])
@@ -6,7 +6,7 @@ build-backend = "pdm.backend"
6
6
 
7
7
  [project]
8
8
  name = "cua-computer"
9
- version = "0.2.2"
9
+ version = "0.2.4"
10
10
  description = "Computer-Use Interface (CUI) framework powering Cua"
11
11
  readme = "README.md"
12
12
  authors = [
@@ -57,7 +57,7 @@ target-version = [
57
57
 
58
58
  [tool.ruff]
59
59
  line-length = 100
60
- target-version = "0.2.2"
60
+ target-version = "0.2.4"
61
61
  select = [
62
62
  "E",
63
63
  "F",
@@ -71,7 +71,7 @@ docstring-code-format = true
71
71
 
72
72
  [tool.mypy]
73
73
  strict = true
74
- python_version = "0.2.2"
74
+ python_version = "0.2.4"
75
75
  ignore_missing_imports = true
76
76
  disallow_untyped_defs = true
77
77
  check_untyped_defs = true
@@ -1,100 +0,0 @@
1
- """Cloud VM provider implementation.
2
-
3
- This module contains a stub implementation for a future cloud VM provider.
4
- """
5
-
6
- import logging
7
- from typing import Dict, List, Optional, Any
8
-
9
- from ..base import BaseVMProvider, VMProviderType
10
-
11
- # Setup logging
12
- logger = logging.getLogger(__name__)
13
-
14
- class CloudProvider(BaseVMProvider):
15
- """Cloud VM Provider stub implementation.
16
-
17
- This is a placeholder for a future cloud VM provider implementation.
18
- """
19
-
20
- def __init__(
21
- self,
22
- host: str = "localhost",
23
- port: int = 7777,
24
- storage: Optional[str] = None,
25
- verbose: bool = False,
26
- ):
27
- """Initialize the Cloud provider.
28
-
29
- Args:
30
- host: Host to use for API connections (default: localhost)
31
- port: Port for the API server (default: 7777)
32
- storage: Path to store VM data
33
- verbose: Enable verbose logging
34
- """
35
- self.host = host
36
- self.port = port
37
- self.storage = storage
38
- self.verbose = verbose
39
-
40
- logger.warning("CloudProvider is not yet implemented")
41
-
42
- @property
43
- def provider_type(self) -> VMProviderType:
44
- """Get the provider type."""
45
- return VMProviderType.CLOUD
46
-
47
- async def __aenter__(self):
48
- """Enter async context manager."""
49
- logger.debug("Entering CloudProvider context")
50
- return self
51
-
52
- async def __aexit__(self, exc_type, exc_val, exc_tb):
53
- """Exit async context manager."""
54
- logger.debug("Exiting CloudProvider context")
55
-
56
- async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
57
- """Get VM information by name."""
58
- logger.warning("CloudProvider.get_vm is not implemented")
59
- return {
60
- "name": name,
61
- "status": "unavailable",
62
- "message": "CloudProvider is not implemented"
63
- }
64
-
65
- async def list_vms(self) -> List[Dict[str, Any]]:
66
- """List all available VMs."""
67
- logger.warning("CloudProvider.list_vms is not implemented")
68
- return []
69
-
70
- async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
71
- """Run a VM with the given options."""
72
- logger.warning("CloudProvider.run_vm is not implemented")
73
- return {
74
- "name": name,
75
- "status": "unavailable",
76
- "message": "CloudProvider is not implemented"
77
- }
78
-
79
- async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
80
- """Stop a running VM."""
81
- logger.warning("CloudProvider.stop_vm is not implemented")
82
- return {
83
- "name": name,
84
- "status": "stopped",
85
- "message": "CloudProvider is not implemented"
86
- }
87
-
88
- async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
89
- """Update VM configuration."""
90
- logger.warning("CloudProvider.update_vm is not implemented")
91
- return {
92
- "name": name,
93
- "status": "unchanged",
94
- "message": "CloudProvider is not implemented"
95
- }
96
-
97
- async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str:
98
- """Get the IP address of a VM."""
99
- logger.warning("CloudProvider.get_ip is not implemented")
100
- raise NotImplementedError("CloudProvider.get_ip is not implemented")
File without changes