cua-computer 0.2.10__py3-none-any.whl → 0.2.11__py3-none-any.whl

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.
@@ -10,6 +10,7 @@ class VMProviderType(StrEnum):
10
10
  LUME = "lume"
11
11
  LUMIER = "lumier"
12
12
  CLOUD = "cloud"
13
+ WINSANDBOX = "winsandbox"
13
14
  UNKNOWN = "unknown"
14
15
 
15
16
 
@@ -112,5 +112,27 @@ class VMProviderFactory:
112
112
  "The CloudProvider is not fully implemented yet. "
113
113
  "Please use LUME or LUMIER provider instead."
114
114
  ) from e
115
+ elif provider_type == VMProviderType.WINSANDBOX:
116
+ try:
117
+ from .winsandbox import WinSandboxProvider, HAS_WINSANDBOX
118
+ if not HAS_WINSANDBOX:
119
+ raise ImportError(
120
+ "pywinsandbox is required for WinSandboxProvider. "
121
+ "Please install it with 'pip install -U git+https://github.com/karkason/pywinsandbox.git'"
122
+ )
123
+ return WinSandboxProvider(
124
+ port=port,
125
+ host=host,
126
+ storage=storage,
127
+ verbose=verbose,
128
+ ephemeral=ephemeral,
129
+ **kwargs
130
+ )
131
+ except ImportError as e:
132
+ logger.error(f"Failed to import WinSandboxProvider: {e}")
133
+ raise ImportError(
134
+ "pywinsandbox is required for WinSandboxProvider. "
135
+ "Please install it with 'pip install -U git+https://github.com/karkason/pywinsandbox.git'"
136
+ ) from e
115
137
  else:
116
138
  raise ValueError(f"Unsupported provider type: {provider_type}")
@@ -0,0 +1,11 @@
1
+ """Windows Sandbox provider for CUA Computer."""
2
+
3
+ try:
4
+ import winsandbox
5
+ HAS_WINSANDBOX = True
6
+ except ImportError:
7
+ HAS_WINSANDBOX = False
8
+
9
+ from .provider import WinSandboxProvider
10
+
11
+ __all__ = ["WinSandboxProvider", "HAS_WINSANDBOX"]
@@ -0,0 +1,468 @@
1
+ """Windows Sandbox VM provider implementation using pywinsandbox."""
2
+
3
+ import os
4
+ import asyncio
5
+ import logging
6
+ import time
7
+ from typing import Dict, Any, Optional, List
8
+
9
+ from ..base import BaseVMProvider, VMProviderType
10
+
11
+ # Setup logging
12
+ logger = logging.getLogger(__name__)
13
+
14
+ try:
15
+ import winsandbox
16
+ HAS_WINSANDBOX = True
17
+ except ImportError:
18
+ HAS_WINSANDBOX = False
19
+
20
+
21
+ class WinSandboxProvider(BaseVMProvider):
22
+ """Windows Sandbox VM provider implementation using pywinsandbox.
23
+
24
+ This provider uses Windows Sandbox to create isolated Windows environments.
25
+ Storage is always ephemeral with Windows Sandbox.
26
+ """
27
+
28
+ def __init__(
29
+ self,
30
+ port: int = 7777,
31
+ host: str = "localhost",
32
+ storage: Optional[str] = None,
33
+ verbose: bool = False,
34
+ ephemeral: bool = True, # Windows Sandbox is always ephemeral
35
+ memory_mb: int = 4096,
36
+ networking: bool = True,
37
+ **kwargs
38
+ ):
39
+ """Initialize the Windows Sandbox provider.
40
+
41
+ Args:
42
+ port: Port for the computer server (default: 7777)
43
+ host: Host to use for connections (default: localhost)
44
+ storage: Storage path (ignored - Windows Sandbox is always ephemeral)
45
+ verbose: Enable verbose logging
46
+ ephemeral: Always True for Windows Sandbox
47
+ memory_mb: Memory allocation in MB (default: 4096)
48
+ networking: Enable networking in sandbox (default: True)
49
+ """
50
+ if not HAS_WINSANDBOX:
51
+ raise ImportError(
52
+ "pywinsandbox is required for WinSandboxProvider. "
53
+ "Please install it with 'pip install pywinsandbox'"
54
+ )
55
+
56
+ self.host = host
57
+ self.port = port
58
+ self.verbose = verbose
59
+ self.memory_mb = memory_mb
60
+ self.networking = networking
61
+
62
+ # Windows Sandbox is always ephemeral
63
+ if not ephemeral:
64
+ logger.warning("Windows Sandbox storage is always ephemeral. Ignoring ephemeral=False.")
65
+ self.ephemeral = True
66
+
67
+ # Storage is always ephemeral for Windows Sandbox
68
+ if storage and storage != "ephemeral":
69
+ logger.warning("Windows Sandbox does not support persistent storage. Using ephemeral storage.")
70
+ self.storage = "ephemeral"
71
+
72
+ self.logger = logging.getLogger(__name__)
73
+
74
+ # Track active sandboxes
75
+ self._active_sandboxes: Dict[str, Any] = {}
76
+
77
+ @property
78
+ def provider_type(self) -> VMProviderType:
79
+ """Get the provider type."""
80
+ return VMProviderType.WINSANDBOX
81
+
82
+ async def __aenter__(self):
83
+ """Enter async context manager."""
84
+ # Verify Windows Sandbox is available
85
+ if not HAS_WINSANDBOX:
86
+ raise ImportError("pywinsandbox is not available")
87
+
88
+ return self
89
+
90
+ async def __aexit__(self, exc_type, exc_val, exc_tb):
91
+ """Exit async context manager."""
92
+ # Clean up any active sandboxes
93
+ for name, sandbox in self._active_sandboxes.items():
94
+ try:
95
+ sandbox.shutdown()
96
+ self.logger.info(f"Terminated sandbox: {name}")
97
+ except Exception as e:
98
+ self.logger.error(f"Error terminating sandbox {name}: {e}")
99
+
100
+ self._active_sandboxes.clear()
101
+
102
+ async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
103
+ """Get VM information by name.
104
+
105
+ Args:
106
+ name: Name of the VM to get information for
107
+ storage: Ignored for Windows Sandbox (always ephemeral)
108
+
109
+ Returns:
110
+ Dictionary with VM information including status, IP address, etc.
111
+ """
112
+ if name not in self._active_sandboxes:
113
+ return {
114
+ "name": name,
115
+ "status": "stopped",
116
+ "ip_address": None,
117
+ "storage": "ephemeral"
118
+ }
119
+
120
+ sandbox = self._active_sandboxes[name]
121
+
122
+ # Check if sandbox is still running
123
+ try:
124
+ # Try to ping the sandbox to see if it's responsive
125
+ try:
126
+ sandbox.rpyc.modules.os.getcwd()
127
+ sandbox_responsive = True
128
+ except Exception:
129
+ sandbox_responsive = False
130
+
131
+ if not sandbox_responsive:
132
+ return {
133
+ "name": name,
134
+ "status": "starting",
135
+ "ip_address": None,
136
+ "storage": "ephemeral",
137
+ "memory_mb": self.memory_mb,
138
+ "networking": self.networking
139
+ }
140
+
141
+ # Check for computer server address file
142
+ server_address_file = r"C:\Users\WDAGUtilityAccount\Desktop\shared_windows_sandbox_dir\server_address"
143
+
144
+ try:
145
+ # Check if the server address file exists
146
+ file_exists = sandbox.rpyc.modules.os.path.exists(server_address_file)
147
+
148
+ if file_exists:
149
+ # Read the server address file
150
+ with sandbox.rpyc.builtin.open(server_address_file, 'r') as f:
151
+ server_address = f.read().strip()
152
+
153
+ if server_address and ':' in server_address:
154
+ # Parse IP:port from the file
155
+ ip_address, port = server_address.split(':', 1)
156
+
157
+ # Verify the server is actually responding
158
+ try:
159
+ import socket
160
+ sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
161
+ sock.settimeout(3)
162
+ result = sock.connect_ex((ip_address, int(port)))
163
+ sock.close()
164
+
165
+ if result == 0:
166
+ # Server is responding
167
+ status = "running"
168
+ self.logger.debug(f"Computer server found at {ip_address}:{port}")
169
+ else:
170
+ # Server file exists but not responding
171
+ status = "starting"
172
+ ip_address = None
173
+ except Exception as e:
174
+ self.logger.debug(f"Error checking server connectivity: {e}")
175
+ status = "starting"
176
+ ip_address = None
177
+ else:
178
+ # File exists but doesn't contain valid address
179
+ status = "starting"
180
+ ip_address = None
181
+ else:
182
+ # Server address file doesn't exist yet
183
+ status = "starting"
184
+ ip_address = None
185
+
186
+ except Exception as e:
187
+ self.logger.debug(f"Error checking server address file: {e}")
188
+ status = "starting"
189
+ ip_address = None
190
+
191
+ except Exception as e:
192
+ self.logger.error(f"Error checking sandbox status: {e}")
193
+ status = "error"
194
+ ip_address = None
195
+
196
+ return {
197
+ "name": name,
198
+ "status": status,
199
+ "ip_address": ip_address,
200
+ "storage": "ephemeral",
201
+ "memory_mb": self.memory_mb,
202
+ "networking": self.networking
203
+ }
204
+
205
+ async def list_vms(self) -> List[Dict[str, Any]]:
206
+ """List all available VMs."""
207
+ vms = []
208
+ for name in self._active_sandboxes.keys():
209
+ vm_info = await self.get_vm(name)
210
+ vms.append(vm_info)
211
+ return vms
212
+
213
+ async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
214
+ """Run a VM with the given options.
215
+
216
+ Args:
217
+ image: Image name (ignored for Windows Sandbox - always uses host Windows)
218
+ name: Name of the VM to run
219
+ run_opts: Dictionary of run options (memory, cpu, etc.)
220
+ storage: Ignored for Windows Sandbox (always ephemeral)
221
+
222
+ Returns:
223
+ Dictionary with VM run status and information
224
+ """
225
+ if name in self._active_sandboxes:
226
+ return {
227
+ "success": False,
228
+ "error": f"Sandbox {name} is already running"
229
+ }
230
+
231
+ try:
232
+ # Extract options from run_opts
233
+ memory_mb = run_opts.get("memory_mb", self.memory_mb)
234
+ if isinstance(memory_mb, str):
235
+ # Convert memory string like "4GB" to MB
236
+ if memory_mb.upper().endswith("GB"):
237
+ memory_mb = int(float(memory_mb[:-2]) * 1024)
238
+ elif memory_mb.upper().endswith("MB"):
239
+ memory_mb = int(memory_mb[:-2])
240
+ else:
241
+ memory_mb = self.memory_mb
242
+
243
+ networking = run_opts.get("networking", self.networking)
244
+
245
+ # Create folder mappers if shared directories are specified
246
+ folder_mappers = []
247
+ shared_directories = run_opts.get("shared_directories", [])
248
+ for shared_dir in shared_directories:
249
+ if isinstance(shared_dir, dict):
250
+ host_path = shared_dir.get("hostPath", "")
251
+ elif isinstance(shared_dir, str):
252
+ host_path = shared_dir
253
+ else:
254
+ continue
255
+
256
+ if host_path and os.path.exists(host_path):
257
+ folder_mappers.append(winsandbox.FolderMapper(host_path))
258
+
259
+ self.logger.info(f"Creating Windows Sandbox: {name}")
260
+ self.logger.info(f"Memory: {memory_mb}MB, Networking: {networking}")
261
+ if folder_mappers:
262
+ self.logger.info(f"Shared directories: {len(folder_mappers)}")
263
+
264
+ # Create the sandbox without logon script
265
+ sandbox = winsandbox.new_sandbox(
266
+ memory_mb=str(memory_mb),
267
+ networking=networking,
268
+ folder_mappers=folder_mappers
269
+ )
270
+
271
+ # Store the sandbox
272
+ self._active_sandboxes[name] = sandbox
273
+
274
+ self.logger.info(f"Windows Sandbox {name} created successfully")
275
+
276
+ # Setup the computer server in the sandbox
277
+ await self._setup_computer_server(sandbox, name)
278
+
279
+ return {
280
+ "success": True,
281
+ "name": name,
282
+ "status": "starting",
283
+ "memory_mb": memory_mb,
284
+ "networking": networking,
285
+ "storage": "ephemeral"
286
+ }
287
+
288
+ except Exception as e:
289
+ self.logger.error(f"Failed to create Windows Sandbox {name}: {e}")
290
+ # stack trace
291
+ import traceback
292
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
293
+ return {
294
+ "success": False,
295
+ "error": f"Failed to create sandbox: {str(e)}"
296
+ }
297
+
298
+ async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
299
+ """Stop a running VM.
300
+
301
+ Args:
302
+ name: Name of the VM to stop
303
+ storage: Ignored for Windows Sandbox
304
+
305
+ Returns:
306
+ Dictionary with stop status and information
307
+ """
308
+ if name not in self._active_sandboxes:
309
+ return {
310
+ "success": False,
311
+ "error": f"Sandbox {name} is not running"
312
+ }
313
+
314
+ try:
315
+ sandbox = self._active_sandboxes[name]
316
+
317
+ # Terminate the sandbox
318
+ sandbox.shutdown()
319
+
320
+ # Remove from active sandboxes
321
+ del self._active_sandboxes[name]
322
+
323
+ self.logger.info(f"Windows Sandbox {name} stopped successfully")
324
+
325
+ return {
326
+ "success": True,
327
+ "name": name,
328
+ "status": "stopped"
329
+ }
330
+
331
+ except Exception as e:
332
+ self.logger.error(f"Failed to stop Windows Sandbox {name}: {e}")
333
+ return {
334
+ "success": False,
335
+ "error": f"Failed to stop sandbox: {str(e)}"
336
+ }
337
+
338
+ async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
339
+ """Update VM configuration.
340
+
341
+ Note: Windows Sandbox does not support runtime configuration updates.
342
+ The sandbox must be stopped and restarted with new configuration.
343
+
344
+ Args:
345
+ name: Name of the VM to update
346
+ update_opts: Dictionary of update options
347
+ storage: Ignored for Windows Sandbox
348
+
349
+ Returns:
350
+ Dictionary with update status and information
351
+ """
352
+ return {
353
+ "success": False,
354
+ "error": "Windows Sandbox does not support runtime configuration updates. "
355
+ "Please stop and restart the sandbox with new configuration."
356
+ }
357
+
358
+ async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str:
359
+ """Get the IP address of a VM, waiting indefinitely until it's available.
360
+
361
+ Args:
362
+ name: Name of the VM to get the IP for
363
+ storage: Ignored for Windows Sandbox
364
+ retry_delay: Delay between retries in seconds (default: 2)
365
+
366
+ Returns:
367
+ IP address of the VM when it becomes available
368
+ """
369
+ total_attempts = 0
370
+
371
+ # Loop indefinitely until we get a valid IP
372
+ while True:
373
+ total_attempts += 1
374
+
375
+ # Log retry message but not on first attempt
376
+ if total_attempts > 1:
377
+ self.logger.info(f"Waiting for Windows Sandbox {name} IP address (attempt {total_attempts})...")
378
+
379
+ try:
380
+ # Get VM information
381
+ vm_info = await self.get_vm(name, storage=storage)
382
+
383
+ # Check if we got a valid IP
384
+ ip = vm_info.get("ip_address", None)
385
+ if ip and ip != "unknown" and not ip.startswith("0.0.0.0"):
386
+ self.logger.info(f"Got valid Windows Sandbox IP address: {ip}")
387
+ return ip
388
+
389
+ # Check the VM status
390
+ status = vm_info.get("status", "unknown")
391
+
392
+ # If VM is not running yet, log and wait
393
+ if status != "running":
394
+ self.logger.info(f"Windows Sandbox is not running yet (status: {status}). Waiting...")
395
+ # If VM is running but no IP yet, wait and retry
396
+ else:
397
+ self.logger.info("Windows Sandbox is running but no valid IP address yet. Waiting...")
398
+
399
+ except Exception as e:
400
+ self.logger.warning(f"Error getting Windows Sandbox {name} IP: {e}, continuing to wait...")
401
+
402
+ # Wait before next retry
403
+ await asyncio.sleep(retry_delay)
404
+
405
+ # Add progress log every 10 attempts
406
+ if total_attempts % 10 == 0:
407
+ self.logger.info(f"Still waiting for Windows Sandbox {name} IP after {total_attempts} attempts...")
408
+
409
+ async def _setup_computer_server(self, sandbox, name: str, visible: bool = False):
410
+ """Setup the computer server in the Windows Sandbox using RPyC.
411
+
412
+ Args:
413
+ sandbox: The Windows Sandbox instance
414
+ name: Name of the sandbox
415
+ visible: Whether the opened process should be visible (default: False)
416
+ """
417
+ try:
418
+ self.logger.info(f"Setting up computer server in sandbox {name}...")
419
+
420
+ # Read the PowerShell setup script
421
+ script_path = os.path.join(os.path.dirname(__file__), "setup_script.ps1")
422
+ with open(script_path, 'r', encoding='utf-8') as f:
423
+ setup_script_content = f.read()
424
+
425
+ # Write the setup script to the sandbox using RPyC
426
+ script_dest_path = r"C:\Users\WDAGUtilityAccount\setup_cua.ps1"
427
+
428
+ self.logger.info(f"Writing setup script to {script_dest_path}")
429
+ with sandbox.rpyc.builtin.open(script_dest_path, 'w') as f:
430
+ f.write(setup_script_content)
431
+
432
+ # Execute the PowerShell script in the background
433
+ self.logger.info("Executing setup script in sandbox...")
434
+
435
+ # Use subprocess to run PowerShell script
436
+ import subprocess
437
+ powershell_cmd = [
438
+ "powershell.exe",
439
+ "-ExecutionPolicy", "Bypass",
440
+ "-NoExit", # Keep window open after script completes
441
+ "-File", script_dest_path
442
+ ]
443
+
444
+ # Set creation flags based on visibility preference
445
+ if visible:
446
+ # CREATE_NEW_CONSOLE - creates a new console window (visible)
447
+ creation_flags = 0x00000010
448
+ else:
449
+ creation_flags = 0x08000000 # CREATE_NO_WINDOW
450
+
451
+ # Start the process using RPyC
452
+ process = sandbox.rpyc.modules.subprocess.Popen(
453
+ powershell_cmd,
454
+ creationflags=creation_flags,
455
+ shell=False
456
+ )
457
+
458
+ # # Sleep for 30 seconds
459
+ # await asyncio.sleep(30)
460
+
461
+ ip = await self.get_ip(name)
462
+ self.logger.info(f"Sandbox IP: {ip}")
463
+ self.logger.info(f"Setup script started in background in sandbox {name} with PID: {process.pid}")
464
+
465
+ except Exception as e:
466
+ self.logger.error(f"Failed to setup computer server in sandbox {name}: {e}")
467
+ import traceback
468
+ self.logger.error(f"Stack trace: {traceback.format_exc()}")
@@ -0,0 +1,124 @@
1
+ # Setup script for Windows Sandbox CUA Computer provider
2
+ # This script runs when the sandbox starts
3
+
4
+ Write-Host "Starting CUA Computer setup in Windows Sandbox..."
5
+
6
+ # Function to find the mapped Python installation from pywinsandbox
7
+ function Find-MappedPython {
8
+ Write-Host "Looking for mapped Python installation from pywinsandbox..."
9
+
10
+ # pywinsandbox maps the host Python installation to the sandbox
11
+ # Look for mapped shared folders on the desktop (common pywinsandbox pattern)
12
+ $desktopPath = "C:\Users\WDAGUtilityAccount\Desktop"
13
+ $sharedFolders = Get-ChildItem -Path $desktopPath -Directory -ErrorAction SilentlyContinue
14
+
15
+ foreach ($folder in $sharedFolders) {
16
+ # Look for Python executables in shared folders
17
+ $pythonPaths = @(
18
+ "$($folder.FullName)\python.exe",
19
+ "$($folder.FullName)\Scripts\python.exe",
20
+ "$($folder.FullName)\bin\python.exe"
21
+ )
22
+
23
+ foreach ($pythonPath in $pythonPaths) {
24
+ if (Test-Path $pythonPath) {
25
+ try {
26
+ $version = & $pythonPath --version 2>&1
27
+ if ($version -match "Python") {
28
+ Write-Host "Found mapped Python: $pythonPath - $version"
29
+ return $pythonPath
30
+ }
31
+ } catch {
32
+ continue
33
+ }
34
+ }
35
+ }
36
+
37
+ # Also check subdirectories that might contain Python
38
+ $subDirs = Get-ChildItem -Path $folder.FullName -Directory -ErrorAction SilentlyContinue
39
+ foreach ($subDir in $subDirs) {
40
+ $pythonPath = "$($subDir.FullName)\python.exe"
41
+ if (Test-Path $pythonPath) {
42
+ try {
43
+ $version = & $pythonPath --version 2>&1
44
+ if ($version -match "Python") {
45
+ Write-Host "Found mapped Python in subdirectory: $pythonPath - $version"
46
+ return $pythonPath
47
+ }
48
+ } catch {
49
+ continue
50
+ }
51
+ }
52
+ }
53
+ }
54
+
55
+ # Fallback: try common Python commands that might be available
56
+ $pythonCommands = @("python", "py", "python3")
57
+ foreach ($cmd in $pythonCommands) {
58
+ try {
59
+ $version = & $cmd --version 2>&1
60
+ if ($version -match "Python") {
61
+ Write-Host "Found Python via command '$cmd': $version"
62
+ return $cmd
63
+ }
64
+ } catch {
65
+ continue
66
+ }
67
+ }
68
+
69
+ throw "Could not find any Python installation (mapped or otherwise)"
70
+ }
71
+
72
+ try {
73
+ # Step 1: Find the mapped Python installation
74
+ Write-Host "Step 1: Finding mapped Python installation..."
75
+ $pythonExe = Find-MappedPython
76
+ Write-Host "Using Python: $pythonExe"
77
+
78
+ # Verify Python works and show version
79
+ $pythonVersion = & $pythonExe --version 2>&1
80
+ Write-Host "Python version: $pythonVersion"
81
+
82
+ # Step 2: Install cua-computer-server directly
83
+ Write-Host "Step 2: Installing cua-computer-server..."
84
+
85
+ Write-Host "Upgrading pip..."
86
+ & $pythonExe -m pip install --upgrade pip --quiet
87
+
88
+ Write-Host "Installing cua-computer-server..."
89
+ & $pythonExe -m pip install cua-computer-server --quiet
90
+
91
+ Write-Host "cua-computer-server installation completed."
92
+
93
+ # Step 3: Start computer server in background
94
+ Write-Host "Step 3: Starting computer server in background..."
95
+ Write-Host "Starting computer server with: $pythonExe"
96
+
97
+ # Start the computer server in the background
98
+ $serverProcess = Start-Process -FilePath $pythonExe -ArgumentList "-m", "computer_server.main" -WindowStyle Hidden -PassThru
99
+ Write-Host "Computer server started in background with PID: $($serverProcess.Id)"
100
+
101
+ # Give it a moment to start
102
+ Start-Sleep -Seconds 3
103
+
104
+ # Check if the process is still running
105
+ if (Get-Process -Id $serverProcess.Id -ErrorAction SilentlyContinue) {
106
+ Write-Host "Computer server is running successfully in background"
107
+ } else {
108
+ throw "Computer server failed to start or exited immediately"
109
+ }
110
+
111
+ } catch {
112
+ Write-Error "Setup failed: $_"
113
+ Write-Host "Error details: $($_.Exception.Message)"
114
+ Write-Host "Stack trace: $($_.ScriptStackTrace)"
115
+ Write-Host ""
116
+ Write-Host "Press any key to close this window..."
117
+ $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
118
+ exit 1
119
+ }
120
+
121
+ Write-Host ""
122
+ Write-Host "Setup completed successfully!"
123
+ Write-Host "Press any key to close this window..."
124
+ $null = $Host.UI.RawUI.ReadKey("NoEcho,IncludeKeyDown")
@@ -0,0 +1,15 @@
1
+ """
2
+ Main entry point for computer.ui module.
3
+
4
+ This allows running the computer UI with:
5
+ python -m computer.ui
6
+
7
+ Instead of:
8
+ python -m computer.ui.gradio.app
9
+ """
10
+
11
+ from .gradio.app import create_gradio_ui
12
+
13
+ if __name__ == "__main__":
14
+ app = create_gradio_ui()
15
+ app.launch()