diff --git a/libs/python/computer/computer/computer.py b/libs/python/computer/computer/computer.py index 7bf0717f..73723b26 100644 --- a/libs/python/computer/computer/computer.py +++ b/libs/python/computer/computer/computer.py @@ -74,8 +74,9 @@ class Computer: verbosity: Union[int, LogLevel] = logging.INFO, telemetry_enabled: bool = True, provider_type: Union[str, VMProviderType] = VMProviderType.LUME, - port: Optional[int] = 7777, + provider_port: Optional[int] = 7777, noVNC_port: Optional[int] = 8006, + api_port: Optional[int] = None, host: str = os.environ.get("PYLUME_HOST", "localhost"), storage: Optional[str] = None, ephemeral: bool = False, @@ -122,14 +123,19 @@ class Computer: # Store original parameters self.image = image - self.port = port + self.provider_port = provider_port self.noVNC_port = noVNC_port + self.api_port = api_port self.host = host self.os_type = os_type self.provider_type = provider_type self.ephemeral = ephemeral + self.api_key = api_key if self.provider_type == VMProviderType.CLOUD else None + + # Set default API port if not specified + if self.api_port is None: + self.api_port = 8443 if self.api_key else 8000 - self.api_key = api_key self.experiments = experiments or [] if "app-use" in self.experiments: @@ -277,7 +283,7 @@ class Computer: interface = cast( BaseComputerInterface, InterfaceFactory.create_interface_for_os( - os=self.os_type, ip_address=ip_address # type: ignore[arg-type] + os=self.os_type, ip_address=ip_address, api_port=self.api_port # type: ignore[arg-type] ), ) self._interface = interface @@ -304,7 +310,7 @@ class Computer: storage = "ephemeral" if self.ephemeral else self.storage verbose = self.verbosity >= LogLevel.DEBUG ephemeral = self.ephemeral - port = self.port if self.port is not None else 7777 + port = self.provider_port if self.provider_port is not None else 7777 host = self.host if self.host else "localhost" image = self.image shared_path = self.shared_path @@ -517,13 +523,14 @@ class Computer: ip_address=ip_address, api_key=self.api_key, vm_name=self.config.name, + api_port=self.api_port, ), ) else: interface = cast( BaseComputerInterface, InterfaceFactory.create_interface_for_os( - os=self.os_type, ip_address=ip_address + os=self.os_type, ip_address=ip_address, api_port=self.api_port ), ) @@ -692,6 +699,7 @@ class Computer: ip_address=ip_address, api_key=self.api_key, vm_name=self.config.name, + api_port=self.api_port, ), ) else: @@ -700,6 +708,7 @@ class Computer: InterfaceFactory.create_interface_for_os( os=self.os_type, ip_address=ip_address, + api_port=self.api_port, ), ) diff --git a/libs/python/computer/computer/interface/factory.py b/libs/python/computer/computer/interface/factory.py index 7ae6b05c..b4c21958 100644 --- a/libs/python/computer/computer/interface/factory.py +++ b/libs/python/computer/computer/interface/factory.py @@ -12,6 +12,7 @@ class InterfaceFactory: def create_interface_for_os( os: Literal["macos", "linux", "windows"], ip_address: str, + api_port: Optional[int] = None, api_key: Optional[str] = None, vm_name: Optional[str] = None, ) -> BaseComputerInterface: @@ -20,6 +21,7 @@ class InterfaceFactory: Args: os: Operating system type ('macos', 'linux', or 'windows') ip_address: IP address of the computer to control + api_port: Optional API port of the computer to control api_key: Optional API key for cloud authentication vm_name: Optional VM name for cloud authentication @@ -35,10 +37,10 @@ class InterfaceFactory: from .windows import WindowsComputerInterface if os == "macos": - return MacOSComputerInterface(ip_address, api_key=api_key, vm_name=vm_name) + return MacOSComputerInterface(ip_address, api_key=api_key, vm_name=vm_name, api_port=api_port) elif os == "linux": - return LinuxComputerInterface(ip_address, api_key=api_key, vm_name=vm_name) + return LinuxComputerInterface(ip_address, api_key=api_key, vm_name=vm_name, api_port=api_port) elif os == "windows": - return WindowsComputerInterface(ip_address, api_key=api_key, vm_name=vm_name) + return WindowsComputerInterface(ip_address, api_key=api_key, vm_name=vm_name, api_port=api_port) else: raise ValueError(f"Unsupported OS type: {os}") diff --git a/libs/python/computer/computer/interface/generic.py b/libs/python/computer/computer/interface/generic.py index e58719dd..22ea2b1a 100644 --- a/libs/python/computer/computer/interface/generic.py +++ b/libs/python/computer/computer/interface/generic.py @@ -30,6 +30,7 @@ class GenericComputerInterface(BaseComputerInterface): api_key: Optional[str] = None, vm_name: Optional[str] = None, logger_name: str = "computer.interface.generic", + api_port: Optional[int] = None, ): super().__init__(ip_address, username, password, api_key, vm_name) self._ws = None @@ -46,6 +47,9 @@ class GenericComputerInterface(BaseComputerInterface): # Set logger name for the interface self.logger = Logger(logger_name, LogLevel.NORMAL) + + # Store custom ports + self._api_port = api_port # Optional default delay time between commands (in seconds) self.delay = 0.0 @@ -70,7 +74,8 @@ class GenericComputerInterface(BaseComputerInterface): WebSocket URI for the Computer API Server """ protocol = "wss" if self.api_key else "ws" - port = "8443" if self.api_key else "8000" + # Use custom API port if provided, otherwise use defaults based on API key + port = str(self._api_port) if self._api_port is not None else ("8443" if self.api_key else "8000") return f"{protocol}://{self.ip_address}:{port}/ws" @property @@ -81,7 +86,8 @@ class GenericComputerInterface(BaseComputerInterface): REST URI for the Computer API Server """ protocol = "https" if self.api_key else "http" - port = "8443" if self.api_key else "8000" + # Use custom API port if provided, otherwise use defaults based on API key + port = str(self._api_port) if self._api_port is not None else ("8443" if self.api_key else "8000") return f"{protocol}://{self.ip_address}:{port}/cmd" # Mouse actions diff --git a/libs/python/computer/computer/interface/linux.py b/libs/python/computer/computer/interface/linux.py index 9e5a3c9b..2dc2a5e7 100644 --- a/libs/python/computer/computer/interface/linux.py +++ b/libs/python/computer/computer/interface/linux.py @@ -13,7 +13,8 @@ class LinuxComputerInterface(GenericComputerInterface): password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None, + api_port: Optional[int] = None, ): super().__init__( - ip_address, username, password, api_key, vm_name, "computer.interface.linux" + ip_address, username, password, api_key, vm_name, "computer.interface.linux", api_port ) diff --git a/libs/python/computer/computer/interface/macos.py b/libs/python/computer/computer/interface/macos.py index 6dcf8a1b..10783c2f 100644 --- a/libs/python/computer/computer/interface/macos.py +++ b/libs/python/computer/computer/interface/macos.py @@ -13,9 +13,10 @@ class MacOSComputerInterface(GenericComputerInterface): password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None, + api_port: Optional[int] = None, ): super().__init__( - ip_address, username, password, api_key, vm_name, "computer.interface.macos" + ip_address, username, password, api_key, vm_name, "computer.interface.macos", api_port ) async def diorama_cmd(self, action: str, arguments: Optional[dict] = None) -> dict: diff --git a/libs/python/computer/computer/interface/windows.py b/libs/python/computer/computer/interface/windows.py index 558ad749..918e7ace 100644 --- a/libs/python/computer/computer/interface/windows.py +++ b/libs/python/computer/computer/interface/windows.py @@ -13,7 +13,8 @@ class WindowsComputerInterface(GenericComputerInterface): password: str = "lume", api_key: Optional[str] = None, vm_name: Optional[str] = None, + api_port: Optional[int] = None, ): super().__init__( - ip_address, username, password, api_key, vm_name, "computer.interface.windows" + ip_address, username, password, api_key, vm_name, "computer.interface.windows", api_port ) diff --git a/libs/python/computer/computer/providers/docker/provider.py b/libs/python/computer/computer/providers/docker/provider.py index e5f56dc5..50dd8393 100644 --- a/libs/python/computer/computer/providers/docker/provider.py +++ b/libs/python/computer/computer/providers/docker/provider.py @@ -37,7 +37,6 @@ class DockerProvider(BaseVMProvider): def __init__( self, - port: Optional[int] = 8000, host: str = "localhost", storage: Optional[str] = None, shared_path: Optional[str] = None, @@ -45,11 +44,11 @@ class DockerProvider(BaseVMProvider): verbose: bool = False, ephemeral: bool = False, vnc_port: Optional[int] = 6901, + api_port: Optional[int] = None, ): """Initialize the Docker VM Provider. Args: - port: Currently unused (VM provider port) host: Hostname for the API server (default: localhost) storage: Path for persistent VM storage shared_path: Path for shared folder between host and container @@ -60,9 +59,10 @@ class DockerProvider(BaseVMProvider): verbose: Enable verbose logging ephemeral: Use ephemeral (temporary) storage vnc_port: Port for VNC interface (default: 6901) + api_port: Port for API server (default: 8000) """ self.host = host - self.api_port = 8000 + self.api_port = api_port if api_port is not None else 8000 self.vnc_port = vnc_port self.ephemeral = ephemeral @@ -296,6 +296,7 @@ class DockerProvider(BaseVMProvider): if vnc_port: cmd.extend(["-p", f"{vnc_port}:6901"]) # VNC port if api_port: + # Map the API port to container port 8000 (computer-server default) cmd.extend(["-p", f"{api_port}:8000"]) # computer-server API port # Add volume mounts if storage is specified diff --git a/libs/python/computer/computer/providers/factory.py b/libs/python/computer/computer/providers/factory.py index 634b4293..11de351f 100644 --- a/libs/python/computer/computer/providers/factory.py +++ b/libs/python/computer/computer/providers/factory.py @@ -14,7 +14,7 @@ class VMProviderFactory: @staticmethod def create_provider( provider_type: Union[str, VMProviderType], - port: int = 7777, + provider_port: int = 7777, host: str = "localhost", bin_path: Optional[str] = None, storage: Optional[str] = None, @@ -23,13 +23,14 @@ class VMProviderFactory: verbose: bool = False, ephemeral: bool = False, noVNC_port: Optional[int] = None, + api_port: Optional[int] = None, **kwargs, ) -> BaseVMProvider: """Create a VM provider of the specified type. Args: provider_type: Type of VM provider to create - port: Port for the API server + provider_port: Port for the provider's API server host: Hostname for the API server bin_path: Path to provider binary if needed storage: Path for persistent VM storage @@ -37,7 +38,8 @@ class VMProviderFactory: image: VM image to use (for Lumier provider) verbose: Enable verbose logging ephemeral: Use ephemeral (temporary) storage - noVNC_port: Specific port for noVNC interface (for Lumier provider) + noVNC_port: Specific port for noVNC interface (for Lumier and Docker provider) + api_port: Specific port for Computer API server (for Docker provider) Returns: An instance of the requested VM provider @@ -63,7 +65,7 @@ class VMProviderFactory: "Please install it with 'pip install cua-computer[lume]'" ) return LumeProvider( - port=port, host=host, storage=storage, verbose=verbose, ephemeral=ephemeral + provider_port=provider_port, host=host, storage=storage, verbose=verbose, ephemeral=ephemeral ) except ImportError as e: logger.error(f"Failed to import LumeProvider: {e}") @@ -81,7 +83,7 @@ class VMProviderFactory: "Please install Docker for Apple Silicon and Lume CLI before using this provider." ) return LumierProvider( - port=port, + provider_port=provider_port, host=host, storage=storage, shared_path=shared_path, @@ -121,7 +123,6 @@ class VMProviderFactory: "Please install it with 'pip install -U git+https://github.com/karkason/pywinsandbox.git'" ) return WinSandboxProvider( - port=port, host=host, storage=storage, verbose=verbose, @@ -144,7 +145,6 @@ class VMProviderFactory: "Please install Docker and ensure it is running." ) return DockerProvider( - port=port, host=host, storage=storage, shared_path=shared_path, @@ -152,6 +152,7 @@ class VMProviderFactory: verbose=verbose, ephemeral=ephemeral, vnc_port=noVNC_port, + api_port=api_port, ) except ImportError as e: logger.error(f"Failed to import DockerProvider: {e}") diff --git a/libs/python/computer/computer/providers/lume/provider.py b/libs/python/computer/computer/providers/lume/provider.py index e9b953c9..535e48ed 100644 --- a/libs/python/computer/computer/providers/lume/provider.py +++ b/libs/python/computer/computer/providers/lume/provider.py @@ -38,7 +38,7 @@ class LumeProvider(BaseVMProvider): def __init__( self, - port: int = 7777, + provider_port: int = 7777, host: str = "localhost", storage: Optional[str] = None, verbose: bool = False, @@ -47,7 +47,7 @@ class LumeProvider(BaseVMProvider): """Initialize the Lume provider. Args: - port: Port for the Lume API server (default: 7777) + provider_port: Port for the Lume API server (default: 7777) host: Host to use for API connections (default: localhost) storage: Path to store VM data verbose: Enable verbose logging @@ -59,7 +59,7 @@ class LumeProvider(BaseVMProvider): ) self.host = host - self.port = port # Default port for Lume API + self.port = provider_port # Default port for Lume API self.storage = storage self.verbose = verbose self.ephemeral = ephemeral # If True, VMs will be deleted after stopping diff --git a/libs/python/computer/computer/providers/lumier/provider.py b/libs/python/computer/computer/providers/lumier/provider.py index d4c99bfe..2740377a 100644 --- a/libs/python/computer/computer/providers/lumier/provider.py +++ b/libs/python/computer/computer/providers/lumier/provider.py @@ -39,7 +39,7 @@ class LumierProvider(BaseVMProvider): def __init__( self, - port: Optional[int] = 7777, + provider_port: Optional[int] = 7777, host: str = "localhost", storage: Optional[str] = None, # Can be a path or 'ephemeral' shared_path: Optional[str] = None, @@ -51,7 +51,7 @@ class LumierProvider(BaseVMProvider): """Initialize the Lumier VM Provider. Args: - port: Port for the API server (default: 7777) + provider_port: Port for the API server (default: 7777) host: Hostname for the API server (default: localhost) storage: Path for persistent VM storage shared_path: Path for shared folder between host and VM @@ -61,8 +61,8 @@ class LumierProvider(BaseVMProvider): noVNC_port: Specific port for noVNC interface (default: 8006) """ self.host = host - # Always ensure api_port has a valid value (7777 is the default) - self.api_port = 7777 if port is None else port + # Always ensure lume_port has a valid value (7777 is the default) + self.lume_port = 7777 if provider_port is None else provider_port self.vnc_port = noVNC_port # User-specified noVNC port, will be set in run_vm if provided self.ephemeral = ephemeral @@ -198,7 +198,7 @@ class LumierProvider(BaseVMProvider): vm_info = lume_api_get( vm_name=name, host=self.host, - port=self.api_port, + port=self.lume_port, storage=storage if storage is not None else self.storage, debug=self.verbose, verbose=self.verbose, @@ -320,7 +320,7 @@ class LumierProvider(BaseVMProvider): logger.debug(f"Using specified noVNC_port: {self.vnc_port}") # Set API URL using the API port - self._api_url = f"http://{self.host}:{self.api_port}" + self._api_url = f"http://{self.host}:{self.lume_port}" # Parse memory setting memory_mb = self._parse_memory(run_opts.get("memory", "8GB")) @@ -671,7 +671,7 @@ class LumierProvider(BaseVMProvider): # Container is running, check if API is responsive try: # First check the health endpoint - api_url = f"http://{self.host}:{self.api_port}/health" + api_url = f"http://{self.host}:{self.lume_port}/health" logger.info(f"Checking API health at: {api_url}") # Use longer timeout for API health check since it may still be initializing @@ -685,7 +685,7 @@ class LumierProvider(BaseVMProvider): else: # API health check failed, now let's check if the VM status endpoint is responsive # This covers cases where the health endpoint isn't implemented but the VM API is working - vm_api_url = f"http://{self.host}:{self.api_port}/lume/vms/{container_name}" + vm_api_url = f"http://{self.host}:{self.lume_port}/lume/vms/{container_name}" if self.storage: import urllib.parse @@ -1026,7 +1026,7 @@ class LumierProvider(BaseVMProvider): # Initialize the API URL with the default value if not already set # This ensures get_vm can work before run_vm is called if not hasattr(self, "_api_url") or not self._api_url: - self._api_url = f"http://{self.host}:{self.api_port}" + self._api_url = f"http://{self.host}:{self.lume_port}" logger.info(f"Initialized default Lumier API URL: {self._api_url}") return self diff --git a/libs/python/computer/computer/providers/winsandbox/provider.py b/libs/python/computer/computer/providers/winsandbox/provider.py index 771ec2b4..863096f1 100644 --- a/libs/python/computer/computer/providers/winsandbox/provider.py +++ b/libs/python/computer/computer/providers/winsandbox/provider.py @@ -29,7 +29,6 @@ class WinSandboxProvider(BaseVMProvider): def __init__( self, - port: int = 7777, host: str = "localhost", storage: Optional[str] = None, verbose: bool = False, @@ -41,7 +40,6 @@ class WinSandboxProvider(BaseVMProvider): """Initialize the Windows Sandbox provider. Args: - port: Port for the computer server (default: 7777) host: Host to use for connections (default: localhost) storage: Storage path (ignored - Windows Sandbox is always ephemeral) verbose: Enable verbose logging @@ -56,7 +54,6 @@ class WinSandboxProvider(BaseVMProvider): ) self.host = host - self.port = port self.verbose = verbose self.memory_mb = memory_mb self.networking = networking