mirror of
https://github.com/trycua/computer.git
synced 2026-01-07 14:00:04 -06:00
Merge pull request #339 from trycua/feat/BYOContainer
[Computer] Add Docker as a local VM provider
This commit is contained in:
@@ -45,34 +45,90 @@ This is a cloud container running the Computer Server. This is the easiest & saf
|
||||
|
||||
## cua local containers
|
||||
|
||||
cua provides local containers. This can be done using either the Lume CLI (macOS) or Docker CLI (Linux, Windows).
|
||||
cua provides local containers using different providers depending on your host operating system:
|
||||
|
||||
### Lume (macOS Only):
|
||||
<Tabs items={['Lume (macOS Only)', 'Windows Sandbox (Windows Only)', 'Docker (macOS, Windows, Linux)']}>
|
||||
<Tab value="Lume (macOS Only)">
|
||||
|
||||
1. Install lume cli
|
||||
|
||||
1. Install lume cli
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
|
||||
```
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
|
||||
```
|
||||
2. Start a local cua container
|
||||
|
||||
2. Start a local cua container
|
||||
```bash
|
||||
lume run macos-sequoia-cua:latest
|
||||
```
|
||||
|
||||
```bash
|
||||
lume run macos-sequoia-cua:latest
|
||||
```
|
||||
3. Connect with Computer
|
||||
|
||||
3. Connect with Computer
|
||||
|
||||
<Tabs items={['Python']}>
|
||||
<Tab value="Python">
|
||||
```python
|
||||
from computer import Computer
|
||||
|
||||
computer = Computer(
|
||||
os_type="macos",
|
||||
provider_type="lume",
|
||||
name="macos-sequoia-cua:latest"
|
||||
)
|
||||
|
||||
await computer.run() # Connect to the container
|
||||
await computer.run() # Launch & connect to the container
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Windows Sandbox (Windows Only)">
|
||||
|
||||
1. Enable Windows Sandbox (requires Windows 10 Pro/Enterprise or Windows 11)
|
||||
2. Install pywinsandbox dependency
|
||||
|
||||
```bash
|
||||
pip install -U git+git://github.com/karkason/pywinsandbox.git
|
||||
```
|
||||
|
||||
3. Windows Sandbox will be automatically configured when you run the CLI
|
||||
|
||||
```python
|
||||
from computer import Computer
|
||||
|
||||
computer = Computer(
|
||||
os_type="windows",
|
||||
provider_type="winsandbox",
|
||||
ephemeral=True # Windows Sandbox is always ephemeral
|
||||
)
|
||||
|
||||
await computer.run() # Launch & connect to Windows Sandbox
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Docker (macOS, Windows, Linux)">
|
||||
|
||||
1. Install Docker Desktop or Docker Engine
|
||||
|
||||
2. Build or pull the CUA Ubuntu container
|
||||
|
||||
```bash
|
||||
# Option 1: Pull from Docker Hub
|
||||
docker pull trycua/cua-ubuntu:latest
|
||||
|
||||
# Option 2: Build locally
|
||||
cd libs/kasm
|
||||
docker build -t cua-ubuntu:latest .
|
||||
```
|
||||
|
||||
3. Connect with Computer
|
||||
|
||||
```python
|
||||
from computer import Computer
|
||||
|
||||
computer = Computer(
|
||||
os_type="linux",
|
||||
provider_type="docker",
|
||||
image="trycua/cua-ubuntu:latest",
|
||||
name="my-cua-container"
|
||||
)
|
||||
|
||||
await computer.run() # Launch & connect to Docker container
|
||||
```
|
||||
|
||||
</Tab>
|
||||
@@ -93,8 +149,8 @@ Connect with:
|
||||
<Tab value="Python">
|
||||
```python
|
||||
|
||||
computer = Computer(use_host_computer_server=True) await
|
||||
computer.run() # Connect to the host desktop
|
||||
computer = Computer(use_host_computer_server=True)
|
||||
await computer.run() # Connect to the host desktop
|
||||
|
||||
```
|
||||
|
||||
|
||||
@@ -21,12 +21,62 @@ cua combines Computer (interface) + Agent (AI) for automating desktop apps. The
|
||||
|
||||
<Step>
|
||||
|
||||
## Create Your First cua Container
|
||||
## Set Up Your Computer Environment
|
||||
|
||||
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
|
||||
2. Navigate to **Dashboard > Containers > Create Instance**
|
||||
3. Create a **Medium, Ubuntu 22** container
|
||||
4. Note your container name and API key
|
||||
Choose how you want to run your cua computer. **Cloud containers are recommended** for the easiest setup:
|
||||
|
||||
<Tabs items={['☁️ Cloud (Recommended)', 'Lume (macOS Only)', 'Windows Sandbox (Windows Only)', 'Docker (Cross-Platform)']}>
|
||||
<Tab value="☁️ Cloud (Recommended)">
|
||||
|
||||
**Easiest & safest way to get started**
|
||||
|
||||
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
|
||||
2. Navigate to **Dashboard > Containers > Create Instance**
|
||||
3. Create a **Medium, Ubuntu 22** container
|
||||
4. Note your container name and API key
|
||||
|
||||
Your cloud container will be automatically configured and ready to use.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Lume (macOS Only)">
|
||||
|
||||
1. Install lume cli
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
|
||||
```
|
||||
|
||||
2. Start a local cua container
|
||||
|
||||
```bash
|
||||
lume run macos-sequoia-cua:latest
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Windows Sandbox (Windows Only)">
|
||||
|
||||
1. Enable Windows Sandbox (requires Windows 10 Pro/Enterprise or Windows 11)
|
||||
2. Install pywinsandbox dependency
|
||||
|
||||
```bash
|
||||
pip install -U git+git://github.com/karkason/pywinsandbox.git
|
||||
```
|
||||
|
||||
3. Windows Sandbox will be automatically configured when you run the CLI
|
||||
|
||||
</Tab>
|
||||
<Tab value="Docker (Cross-Platform)">
|
||||
|
||||
1. Install Docker Desktop or Docker Engine
|
||||
|
||||
2. Pull the CUA Ubuntu container
|
||||
|
||||
```bash
|
||||
docker pull trycua/cua-ubuntu:latest
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
|
||||
@@ -20,12 +20,62 @@ cua combines Computer (interface) + Agent (AI) for automating desktop apps. Comp
|
||||
|
||||
<Step>
|
||||
|
||||
## Create Your First cua Container
|
||||
## Set Up Your Computer Environment
|
||||
|
||||
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
|
||||
2. Navigate to **Dashboard > Containers > Create Instance**
|
||||
3. Create a **Medium, Ubuntu 22** container
|
||||
4. Note your container name and API key
|
||||
Choose how you want to run your cua computer. **Cloud containers are recommended** for the easiest setup:
|
||||
|
||||
<Tabs items={['☁️ Cloud (Recommended)', 'Lume (macOS Only)', 'Windows Sandbox (Windows Only)', 'Docker (Cross-Platform)']}>
|
||||
<Tab value="☁️ Cloud (Recommended)">
|
||||
|
||||
**Easiest & safest way to get started**
|
||||
|
||||
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
|
||||
2. Navigate to **Dashboard > Containers > Create Instance**
|
||||
3. Create a **Medium, Ubuntu 22** container
|
||||
4. Note your container name and API key
|
||||
|
||||
Your cloud container will be automatically configured and ready to use.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Lume (macOS Only)">
|
||||
|
||||
1. Install lume cli
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
|
||||
```
|
||||
|
||||
2. Start a local cua container
|
||||
|
||||
```bash
|
||||
lume run macos-sequoia-cua:latest
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Windows Sandbox (Windows Only)">
|
||||
|
||||
1. Enable Windows Sandbox (requires Windows 10 Pro/Enterprise or Windows 11)
|
||||
2. Install pywinsandbox dependency
|
||||
|
||||
```bash
|
||||
pip install -U git+git://github.com/karkason/pywinsandbox.git
|
||||
```
|
||||
|
||||
3. Windows Sandbox will be automatically configured when you run the CLI
|
||||
|
||||
</Tab>
|
||||
<Tab value="Docker (Cross-Platform)">
|
||||
|
||||
1. Install Docker Desktop or Docker Engine
|
||||
|
||||
2. Pull the CUA Ubuntu container
|
||||
|
||||
```bash
|
||||
docker pull trycua/cua-ubuntu:latest
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -35,9 +85,15 @@ cua combines Computer (interface) + Agent (AI) for automating desktop apps. Comp
|
||||
|
||||
<Tabs items={['Python', 'TypeScript']}>
|
||||
<Tab value="Python">
|
||||
```bash pip install "cua-agent[all]" cua-computer ```
|
||||
```bash
|
||||
pip install "cua-agent[all]" cua-computer
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">
|
||||
```bash
|
||||
npm install @trycua/computer
|
||||
```
|
||||
</Tab>
|
||||
<Tab value="TypeScript">```bash npm install @trycua/computer ```</Tab>
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
@@ -21,12 +21,62 @@ cua combines Computer (interface) + Agent (AI) for automating desktop apps. The
|
||||
|
||||
<Step>
|
||||
|
||||
## Create Your First cua Container
|
||||
## Set Up Your Computer Environment
|
||||
|
||||
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
|
||||
2. Navigate to **Dashboard > Containers > Create Instance**
|
||||
3. Create a **Medium, Ubuntu 22** container
|
||||
4. Note your container name and API key
|
||||
Choose how you want to run your cua computer. **Cloud containers are recommended** for the easiest setup:
|
||||
|
||||
<Tabs items={['☁️ Cloud (Recommended)', 'Lume (macOS Only)', 'Windows Sandbox (Windows Only)', 'Docker (Cross-Platform)']}>
|
||||
<Tab value="☁️ Cloud (Recommended)">
|
||||
|
||||
**Easiest & safest way to get started**
|
||||
|
||||
1. Go to [trycua.com/signin](https://www.trycua.com/signin)
|
||||
2. Navigate to **Dashboard > Containers > Create Instance**
|
||||
3. Create a **Medium, Ubuntu 22** container
|
||||
4. Note your container name and API key
|
||||
|
||||
Your cloud container will be automatically configured and ready to use.
|
||||
|
||||
</Tab>
|
||||
<Tab value="Lume (macOS Only)">
|
||||
|
||||
1. Install lume cli
|
||||
|
||||
```bash
|
||||
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/trycua/cua/main/libs/lume/scripts/install.sh)"
|
||||
```
|
||||
|
||||
2. Start a local cua container
|
||||
|
||||
```bash
|
||||
lume run macos-sequoia-cua:latest
|
||||
```
|
||||
|
||||
</Tab>
|
||||
<Tab value="Windows Sandbox (Windows Only)">
|
||||
|
||||
1. Enable Windows Sandbox (requires Windows 10 Pro/Enterprise or Windows 11)
|
||||
2. Install pywinsandbox dependency
|
||||
|
||||
```bash
|
||||
pip install -U git+git://github.com/karkason/pywinsandbox.git
|
||||
```
|
||||
|
||||
3. Windows Sandbox will be automatically configured when you run the CLI
|
||||
|
||||
</Tab>
|
||||
<Tab value="Docker (Cross-Platform)">
|
||||
|
||||
1. Install Docker Desktop or Docker Engine
|
||||
|
||||
2. Pull the CUA Ubuntu container
|
||||
|
||||
```bash
|
||||
docker pull trycua/cua-ubuntu:latest
|
||||
```
|
||||
|
||||
</Tab>
|
||||
</Tabs>
|
||||
|
||||
</Step>
|
||||
|
||||
|
||||
43
examples/docker_examples.py
Normal file
43
examples/docker_examples.py
Normal file
@@ -0,0 +1,43 @@
|
||||
import asyncio
|
||||
from computer.providers.factory import VMProviderFactory
|
||||
from computer import Computer, VMProviderType
|
||||
import os
|
||||
|
||||
async def main():
|
||||
# # Create docker provider
|
||||
# provider = VMProviderFactory.create_provider(
|
||||
# provider_type="docker",
|
||||
# image="cua-ubuntu:latest", # Your CUA Ubuntu image
|
||||
# port=8080,
|
||||
# vnc_port=6901
|
||||
# )
|
||||
|
||||
# # Run a container
|
||||
# async with provider:
|
||||
# vm_info = await provider.run_vm(
|
||||
# image="cua-ubuntu:latest",
|
||||
# name="my-cua-container",
|
||||
# run_opts={
|
||||
# "memory": "4GB",
|
||||
# "cpu": 2,
|
||||
# "vnc_port": 6901,
|
||||
# "api_port": 8080
|
||||
# }
|
||||
# )
|
||||
# print(vm_info)
|
||||
|
||||
computer = Computer(
|
||||
os_type="linux",
|
||||
provider_type=VMProviderType.DOCKER,
|
||||
name="my-cua-container",
|
||||
image="cua-ubuntu:latest",
|
||||
)
|
||||
|
||||
await computer.run()
|
||||
|
||||
screenshot = await computer.interface.screenshot()
|
||||
with open("screenshot_docker.png", "wb") as f:
|
||||
f.write(screenshot)
|
||||
|
||||
if __name__ == "__main__":
|
||||
asyncio.run(main())
|
||||
37
libs/kasm/Dockerfile
Normal file
37
libs/kasm/Dockerfile
Normal file
@@ -0,0 +1,37 @@
|
||||
FROM kasmweb/core-ubuntu-jammy:1.17.0
|
||||
USER root
|
||||
|
||||
ENV HOME=/home/kasm-default-profile
|
||||
ENV STARTUPDIR=/dockerstartup
|
||||
ENV INST_SCRIPTS=$STARTUPDIR/install
|
||||
WORKDIR $HOME
|
||||
|
||||
######### Customize Container Here ###########
|
||||
|
||||
# Installing python, pip, and libraries
|
||||
RUN apt-get update
|
||||
RUN apt install -y wget build-essential libncursesw5-dev libssl-dev \
|
||||
libsqlite3-dev tk-dev libgdbm-dev libc6-dev libbz2-dev libffi-dev zlib1g-dev
|
||||
RUN add-apt-repository ppa:deadsnakes/ppa
|
||||
RUN apt install -y python3.11 python3-pip python3-tk python3-dev \
|
||||
gnome-screenshot wmctrl ffmpeg socat xclip
|
||||
|
||||
RUN pip install cua-computer-server
|
||||
|
||||
# Disable SSL requirement
|
||||
RUN sed -i 's/require_ssl: true/require_ssl: false/g' /usr/share/kasmvnc/kasmvnc_defaults.yaml
|
||||
RUN sed -i 's/-sslOnly//g' /dockerstartup/vnc_startup.sh
|
||||
|
||||
# Running the python script on startup
|
||||
RUN echo "/usr/bin/python3 -m computer_server" > $STARTUPDIR/custom_startup.sh \
|
||||
&& chmod +x $STARTUPDIR/custom_startup.sh
|
||||
|
||||
######### End Customizations ###########
|
||||
|
||||
RUN chown 1000:0 $HOME
|
||||
RUN $STARTUPDIR/set_user_permission.sh $HOME
|
||||
ENV HOME=/home/kasm-user
|
||||
WORKDIR $HOME
|
||||
RUN mkdir -p $HOME && chown -R 1000:0 $HOME
|
||||
|
||||
USER 1000
|
||||
121
libs/kasm/README.md
Normal file
121
libs/kasm/README.md
Normal file
@@ -0,0 +1,121 @@
|
||||
# CUA Ubuntu Container
|
||||
|
||||
Containerized virtual desktop for Computer-Using Agents (CUA). Utilizes Kasm's MIT-licensed Ubuntu XFCE container as a base with computer-server pre-installed.
|
||||
|
||||
## Features
|
||||
|
||||
- Ubuntu 22.04 (Jammy) with XFCE desktop environment
|
||||
- Pre-installed computer-server for remote computer control
|
||||
- VNC access for visual desktop interaction
|
||||
- Python 3.11 with necessary libraries
|
||||
- Screen capture tools (gnome-screenshot, wmctrl, ffmpeg)
|
||||
- Clipboard utilities (xclip, socat)
|
||||
|
||||
## Usage
|
||||
|
||||
### Building the Container
|
||||
|
||||
```bash
|
||||
docker build -t cua-ubuntu:latest .
|
||||
```
|
||||
|
||||
### Pushing to Registry
|
||||
|
||||
To push the container to a Docker registry:
|
||||
|
||||
```bash
|
||||
# Tag for your registry (replace with your registry URL)
|
||||
docker tag cua-ubuntu:latest your-registry.com/cua-ubuntu:latest
|
||||
|
||||
# Push to registry
|
||||
docker push your-registry.com/cua-ubuntu:latest
|
||||
```
|
||||
|
||||
For Docker Hub:
|
||||
|
||||
```bash
|
||||
# Tag for Docker Hub (replace 'trycua' with your Docker Hub username)
|
||||
docker tag cua-ubuntu:latest trycua/cua-ubuntu:latest
|
||||
|
||||
# Login to Docker Hub
|
||||
docker login
|
||||
|
||||
# Push to Docker Hub
|
||||
docker push trycua/cua-ubuntu:latest
|
||||
```
|
||||
|
||||
### Running the Container Manually
|
||||
|
||||
```bash
|
||||
docker run --rm -it --shm-size=512m -p 6901:6901 -p 8000:8000 -e VNC_PW=password cua-ubuntu:latest
|
||||
```
|
||||
|
||||
- **VNC Access**: Available at `http://localhost:6901` (username: `kasm-user`, password: `password`)
|
||||
- **Computer Server API**: Available at `http://localhost:8000`
|
||||
|
||||
### Using with CUA Docker Provider
|
||||
|
||||
This container is designed to work with the CUA Docker provider for automated container management:
|
||||
|
||||
```python
|
||||
from computer.providers.factory import VMProviderFactory
|
||||
|
||||
# Create docker provider
|
||||
provider = VMProviderFactory.create_provider(
|
||||
provider_type="docker",
|
||||
image="cua-ubuntu:latest",
|
||||
port=8000, # computer-server API port
|
||||
noVNC_port=6901 # VNC port
|
||||
)
|
||||
|
||||
# Run a container
|
||||
async with provider:
|
||||
vm_info = await provider.run_vm(
|
||||
image="cua-ubuntu:latest",
|
||||
name="my-cua-container",
|
||||
run_opts={
|
||||
"memory": "4GB",
|
||||
"cpu": 2,
|
||||
"vnc_port": 6901,
|
||||
"api_port": 8000
|
||||
}
|
||||
)
|
||||
```
|
||||
|
||||
## Container Configuration
|
||||
|
||||
### Ports
|
||||
- **6901**: VNC web interface (noVNC)
|
||||
- **8080**: Computer-server API endpoint
|
||||
|
||||
### Environment Variables
|
||||
- `VNC_PW`: VNC password (default: "password")
|
||||
- `DISPLAY`: X11 display (set to ":0")
|
||||
|
||||
### Volumes
|
||||
- `/home/kasm-user/storage`: Persistent storage mount point
|
||||
- `/home/kasm-user/shared`: Shared folder mount point
|
||||
|
||||
## Creating Snapshots
|
||||
|
||||
You can create a snapshot of the container at any time:
|
||||
|
||||
```bash
|
||||
docker commit <container_id> cua-ubuntu-snapshot:latest
|
||||
```
|
||||
|
||||
Then run the snapshot:
|
||||
|
||||
```bash
|
||||
docker run --rm -it --shm-size=512m -p 6901:6901 -p 8080:8080 -e VNC_PW=password cua-ubuntu-snapshot:latest
|
||||
```
|
||||
|
||||
## Integration with CUA System
|
||||
|
||||
This container integrates seamlessly with the CUA computer provider system:
|
||||
|
||||
- **Automatic Management**: Use the Docker provider for lifecycle management
|
||||
- **Resource Control**: Configure memory, CPU, and storage limits
|
||||
- **Network Access**: Automatic port mapping and IP detection
|
||||
- **Storage Persistence**: Mount host directories for persistent data
|
||||
- **Monitoring**: Real-time container status and health checking
|
||||
@@ -94,7 +94,7 @@ def print_action(action_type: str, details: Dict[str, Any], total_cost: float):
|
||||
# Format action details
|
||||
args_str = ""
|
||||
if action_type == "click" and "x" in details and "y" in details:
|
||||
args_str = f"_{details['button']}({details['x']}, {details['y']})"
|
||||
args_str = f"_{details.get('button', 'left')}({details['x']}, {details['y']})"
|
||||
elif action_type == "type" and "text" in details:
|
||||
text = details["text"]
|
||||
if len(text) > 50:
|
||||
|
||||
@@ -39,6 +39,7 @@ global_agent = None
|
||||
global_computer = None
|
||||
SETTINGS_FILE = Path(".gradio_settings.json")
|
||||
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
|
||||
import dotenv
|
||||
if dotenv.load_dotenv():
|
||||
|
||||
@@ -187,7 +187,7 @@ if __name__ == "__main__":
|
||||
"""
|
||||
<div style="display: flex; justify-content: center; margin-bottom: 0.5em">
|
||||
<img alt="CUA Logo" style="width: 80px;"
|
||||
src="https://github.com/trycua/cua/blob/main/img/logo_black.png?raw=true" />
|
||||
src="https://github.com/trycua/cua/blob/main/img/logo_white.png?raw=true" />
|
||||
</div>
|
||||
"""
|
||||
)
|
||||
@@ -201,22 +201,33 @@ if __name__ == "__main__":
|
||||
)
|
||||
|
||||
with gr.Accordion("Computer Configuration", open=True):
|
||||
computer_os = gr.Radio(
|
||||
choices=["macos", "linux", "windows"],
|
||||
label="Operating System",
|
||||
value="macos",
|
||||
info="Select the operating system for the computer",
|
||||
)
|
||||
|
||||
is_windows = platform.system().lower() == "windows"
|
||||
is_mac = platform.system().lower() == "darwin"
|
||||
|
||||
providers = ["cloud", "localhost"]
|
||||
providers = ["cloud", "localhost", "docker"]
|
||||
if is_mac:
|
||||
providers += ["lume"]
|
||||
if is_windows:
|
||||
providers += ["winsandbox"]
|
||||
|
||||
# Remove unavailable options
|
||||
# MacOS is unavailable if Lume is not available
|
||||
# Windows is unavailable if Winsandbox is not available
|
||||
# Linux is always available
|
||||
# This should be removed once we support macOS and Windows on the cloud provider
|
||||
computer_choices = ["macos", "linux", "windows"]
|
||||
if not is_mac or "lume" not in providers:
|
||||
computer_choices.remove("macos")
|
||||
if not is_windows or "winsandbox" not in providers:
|
||||
computer_choices.remove("windows")
|
||||
|
||||
computer_os = gr.Radio(
|
||||
choices=computer_choices,
|
||||
label="Operating System",
|
||||
value=computer_choices[0],
|
||||
info="Select the operating system for the computer",
|
||||
)
|
||||
|
||||
computer_provider = gr.Radio(
|
||||
choices=providers,
|
||||
label="Provider",
|
||||
|
||||
@@ -43,7 +43,7 @@ class Computer:
|
||||
cpu: str = "4",
|
||||
os_type: OSType = "macos",
|
||||
name: str = "",
|
||||
image: str = "macos-sequoia-cua:latest",
|
||||
image: Optional[str] = None,
|
||||
shared_directories: Optional[List[str]] = None,
|
||||
use_host_computer_server: bool = False,
|
||||
verbosity: Union[int, LogLevel] = logging.INFO,
|
||||
@@ -88,6 +88,12 @@ class Computer:
|
||||
self.logger = Logger("computer", verbosity)
|
||||
self.logger.info("Initializing Computer...")
|
||||
|
||||
if os_type == "macos":
|
||||
image = "macos-sequoia-cua:latest"
|
||||
elif os_type == "linux":
|
||||
image = "trycua/cua-ubuntu:latest"
|
||||
image = str(image)
|
||||
|
||||
# Store original parameters
|
||||
self.image = image
|
||||
self.port = port
|
||||
@@ -301,6 +307,19 @@ class Computer:
|
||||
storage=storage,
|
||||
verbose=verbose,
|
||||
ephemeral=ephemeral,
|
||||
noVNC_port=noVNC_port,
|
||||
)
|
||||
elif self.provider_type == VMProviderType.DOCKER:
|
||||
self.config.vm_provider = VMProviderFactory.create_provider(
|
||||
self.provider_type,
|
||||
port=port,
|
||||
host=host,
|
||||
storage=storage,
|
||||
shared_path=shared_path,
|
||||
image=image or "trycua/cua-ubuntu:latest",
|
||||
verbose=verbose,
|
||||
ephemeral=ephemeral,
|
||||
noVNC_port=noVNC_port,
|
||||
)
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider type: {self.provider_type}")
|
||||
|
||||
@@ -11,6 +11,7 @@ class VMProviderType(StrEnum):
|
||||
LUMIER = "lumier"
|
||||
CLOUD = "cloud"
|
||||
WINSANDBOX = "winsandbox"
|
||||
DOCKER = "docker"
|
||||
UNKNOWN = "unknown"
|
||||
|
||||
|
||||
|
||||
13
libs/python/computer/computer/providers/docker/__init__.py
Normal file
13
libs/python/computer/computer/providers/docker/__init__.py
Normal file
@@ -0,0 +1,13 @@
|
||||
"""Docker provider for running containers with computer-server."""
|
||||
|
||||
from .provider import DockerProvider
|
||||
|
||||
# Check if Docker is available
|
||||
try:
|
||||
import subprocess
|
||||
subprocess.run(["docker", "--version"], capture_output=True, check=True)
|
||||
HAS_DOCKER = True
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
HAS_DOCKER = False
|
||||
|
||||
__all__ = ["DockerProvider", "HAS_DOCKER"]
|
||||
502
libs/python/computer/computer/providers/docker/provider.py
Normal file
502
libs/python/computer/computer/providers/docker/provider.py
Normal file
@@ -0,0 +1,502 @@
|
||||
"""
|
||||
Docker VM provider implementation.
|
||||
|
||||
This provider uses Docker containers running the CUA Ubuntu image to create
|
||||
Linux VMs with computer-server. It handles VM lifecycle operations through Docker
|
||||
commands and container management.
|
||||
"""
|
||||
|
||||
import logging
|
||||
import json
|
||||
import asyncio
|
||||
from typing import Dict, List, Optional, Any
|
||||
import subprocess
|
||||
import time
|
||||
import re
|
||||
|
||||
from ..base import BaseVMProvider, VMProviderType
|
||||
|
||||
# Setup logging
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Check if Docker is available
|
||||
try:
|
||||
subprocess.run(["docker", "--version"], capture_output=True, check=True)
|
||||
HAS_DOCKER = True
|
||||
except (subprocess.SubprocessError, FileNotFoundError):
|
||||
HAS_DOCKER = False
|
||||
|
||||
|
||||
class DockerProvider(BaseVMProvider):
|
||||
"""
|
||||
Docker VM Provider implementation using Docker containers.
|
||||
|
||||
This provider uses Docker to run containers with the CUA Ubuntu image
|
||||
that includes computer-server for remote computer use.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
port: Optional[int] = 8000,
|
||||
host: str = "localhost",
|
||||
storage: Optional[str] = None,
|
||||
shared_path: Optional[str] = None,
|
||||
image: str = "trycua/cua-ubuntu:latest",
|
||||
verbose: bool = False,
|
||||
ephemeral: bool = False,
|
||||
vnc_port: Optional[int] = 6901,
|
||||
):
|
||||
"""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
|
||||
image: Docker image to use (default: "trycua/cua-ubuntu:latest")
|
||||
verbose: Enable verbose logging
|
||||
ephemeral: Use ephemeral (temporary) storage
|
||||
vnc_port: Port for VNC interface (default: 6901)
|
||||
"""
|
||||
self.host = host
|
||||
self.api_port = 8000
|
||||
self.vnc_port = vnc_port
|
||||
self.ephemeral = ephemeral
|
||||
|
||||
# Handle ephemeral storage (temporary directory)
|
||||
if ephemeral:
|
||||
self.storage = "ephemeral"
|
||||
else:
|
||||
self.storage = storage
|
||||
|
||||
self.shared_path = shared_path
|
||||
self.image = image
|
||||
self.verbose = verbose
|
||||
self._container_id = None
|
||||
self._running_containers = {} # Track running containers by name
|
||||
|
||||
@property
|
||||
def provider_type(self) -> VMProviderType:
|
||||
"""Return the provider type."""
|
||||
return VMProviderType.DOCKER
|
||||
|
||||
def _parse_memory(self, memory_str: str) -> str:
|
||||
"""Parse memory string to Docker format.
|
||||
|
||||
Examples:
|
||||
"8GB" -> "8g"
|
||||
"1024MB" -> "1024m"
|
||||
"512" -> "512m"
|
||||
"""
|
||||
if isinstance(memory_str, int):
|
||||
return f"{memory_str}m"
|
||||
|
||||
if isinstance(memory_str, str):
|
||||
# Extract number and unit
|
||||
match = re.match(r"(\d+)([A-Za-z]*)", memory_str)
|
||||
if match:
|
||||
value, unit = match.groups()
|
||||
unit = unit.upper()
|
||||
|
||||
if unit == "GB" or unit == "G":
|
||||
return f"{value}g"
|
||||
elif unit == "MB" or unit == "M" or unit == "":
|
||||
return f"{value}m"
|
||||
|
||||
# Default fallback
|
||||
logger.warning(f"Could not parse memory string '{memory_str}', using 4g default")
|
||||
return "4g" # Default to 4GB
|
||||
|
||||
async def get_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Get VM information by name.
|
||||
|
||||
Args:
|
||||
name: Name of the VM to get information for
|
||||
storage: Optional storage path override. If provided, this will be used
|
||||
instead of the provider's default storage path.
|
||||
|
||||
Returns:
|
||||
Dictionary with VM information including status, IP address, etc.
|
||||
"""
|
||||
try:
|
||||
# Check if container exists and get its status
|
||||
cmd = ["docker", "inspect", name]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True)
|
||||
|
||||
if result.returncode != 0:
|
||||
# Container doesn't exist
|
||||
return {
|
||||
"name": name,
|
||||
"status": "not_found",
|
||||
"ip_address": None,
|
||||
"ports": {},
|
||||
"image": self.image,
|
||||
"provider": "docker"
|
||||
}
|
||||
|
||||
# Parse container info
|
||||
container_info = json.loads(result.stdout)[0]
|
||||
state = container_info["State"]
|
||||
network_settings = container_info["NetworkSettings"]
|
||||
|
||||
# Determine status
|
||||
if state["Running"]:
|
||||
status = "running"
|
||||
elif state["Paused"]:
|
||||
status = "paused"
|
||||
else:
|
||||
status = "stopped"
|
||||
|
||||
# Get IP address
|
||||
ip_address = network_settings.get("IPAddress", "")
|
||||
if not ip_address and "Networks" in network_settings:
|
||||
# Try to get IP from bridge network
|
||||
for network_name, network_info in network_settings["Networks"].items():
|
||||
if network_info.get("IPAddress"):
|
||||
ip_address = network_info["IPAddress"]
|
||||
break
|
||||
|
||||
# Get port mappings
|
||||
ports = {}
|
||||
if "Ports" in network_settings and network_settings["Ports"]:
|
||||
# network_settings["Ports"] is a dict like:
|
||||
# {'6901/tcp': [{'HostIp': '0.0.0.0', 'HostPort': '6901'}, ...], ...}
|
||||
for container_port, port_mappings in network_settings["Ports"].items():
|
||||
if port_mappings: # Check if there are any port mappings
|
||||
# Take the first mapping (usually the IPv4 one)
|
||||
for mapping in port_mappings:
|
||||
if mapping.get("HostPort"):
|
||||
ports[container_port] = mapping["HostPort"]
|
||||
break # Use the first valid mapping
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"status": status,
|
||||
"ip_address": ip_address or "127.0.0.1", # Use localhost if no IP
|
||||
"ports": ports,
|
||||
"image": container_info["Config"]["Image"],
|
||||
"provider": "docker",
|
||||
"container_id": container_info["Id"][:12], # Short ID
|
||||
"created": container_info["Created"],
|
||||
"started": state.get("StartedAt", ""),
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Error getting VM info for {name}: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return {
|
||||
"name": name,
|
||||
"status": "error",
|
||||
"error": str(e),
|
||||
"provider": "docker"
|
||||
}
|
||||
|
||||
async def list_vms(self) -> List[Dict[str, Any]]:
|
||||
"""List all Docker containers managed by this provider."""
|
||||
try:
|
||||
# List all containers (running and stopped) with the CUA image
|
||||
cmd = ["docker", "ps", "-a", "--filter", f"ancestor={self.image}", "--format", "json"]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
|
||||
containers = []
|
||||
if result.stdout.strip():
|
||||
for line in result.stdout.strip().split('\n'):
|
||||
if line.strip():
|
||||
container_data = json.loads(line)
|
||||
vm_info = await self.get_vm(container_data["Names"])
|
||||
containers.append(vm_info)
|
||||
|
||||
return containers
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
logger.error(f"Error listing containers: {e.stderr}")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"Error listing VMs: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
return []
|
||||
|
||||
async def run_vm(self, image: str, name: str, run_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Run a VM with the given options.
|
||||
|
||||
Args:
|
||||
image: Name/tag of the Docker image to use
|
||||
name: Name of the container to run
|
||||
run_opts: Options for running the VM, including:
|
||||
- memory: Memory limit (e.g., "4GB", "2048MB")
|
||||
- cpu: CPU limit (e.g., 2 for 2 cores)
|
||||
- vnc_port: Specific port for VNC interface
|
||||
- api_port: Specific port for computer-server API
|
||||
|
||||
Returns:
|
||||
Dictionary with VM status information
|
||||
"""
|
||||
try:
|
||||
# Check if container already exists
|
||||
existing_vm = await self.get_vm(name, storage)
|
||||
if existing_vm["status"] == "running":
|
||||
logger.info(f"Container {name} is already running")
|
||||
return existing_vm
|
||||
elif existing_vm["status"] in ["stopped", "paused"]:
|
||||
# Start existing container
|
||||
logger.info(f"Starting existing container {name}")
|
||||
start_cmd = ["docker", "start", name]
|
||||
result = subprocess.run(start_cmd, capture_output=True, text=True, check=True)
|
||||
|
||||
# Wait for container to be ready
|
||||
await self._wait_for_container_ready(name)
|
||||
return await self.get_vm(name, storage)
|
||||
|
||||
# Use provided image or default
|
||||
docker_image = image if image != "default" else self.image
|
||||
|
||||
# Build docker run command
|
||||
cmd = ["docker", "run", "-d", "--name", name]
|
||||
|
||||
# Add memory limit if specified
|
||||
if "memory" in run_opts:
|
||||
memory_limit = self._parse_memory(run_opts["memory"])
|
||||
cmd.extend(["--memory", memory_limit])
|
||||
|
||||
# Add CPU limit if specified
|
||||
if "cpu" in run_opts:
|
||||
cpu_count = str(run_opts["cpu"])
|
||||
cmd.extend(["--cpus", cpu_count])
|
||||
|
||||
# Add port mappings
|
||||
vnc_port = run_opts.get("vnc_port", self.vnc_port)
|
||||
api_port = run_opts.get("api_port", self.api_port)
|
||||
|
||||
if vnc_port:
|
||||
cmd.extend(["-p", f"{vnc_port}:6901"]) # VNC port
|
||||
if api_port:
|
||||
cmd.extend(["-p", f"{api_port}:8000"]) # computer-server API port
|
||||
|
||||
# Add volume mounts if storage is specified
|
||||
storage_path = storage or self.storage
|
||||
if storage_path and storage_path != "ephemeral":
|
||||
# Mount storage directory
|
||||
cmd.extend(["-v", f"{storage_path}:/home/kasm-user/storage"])
|
||||
|
||||
# Add shared path if specified
|
||||
if self.shared_path:
|
||||
cmd.extend(["-v", f"{self.shared_path}:/home/kasm-user/shared"])
|
||||
|
||||
# Add environment variables
|
||||
cmd.extend(["-e", "VNC_PW=password"]) # Set VNC password
|
||||
cmd.extend(["-e", "VNCOPTIONS=-disableBasicAuth"]) # Disable VNC basic auth
|
||||
|
||||
# Add the image
|
||||
cmd.append(docker_image)
|
||||
|
||||
logger.info(f"Running Docker container with command: {' '.join(cmd)}")
|
||||
|
||||
# Run the container
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
container_id = result.stdout.strip()
|
||||
|
||||
logger.info(f"Container {name} started with ID: {container_id[:12]}")
|
||||
|
||||
# Store container info
|
||||
self._container_id = container_id
|
||||
self._running_containers[name] = container_id
|
||||
|
||||
# Wait for container to be ready
|
||||
await self._wait_for_container_ready(name)
|
||||
|
||||
# Return VM info
|
||||
vm_info = await self.get_vm(name, storage)
|
||||
vm_info["container_id"] = container_id[:12]
|
||||
|
||||
return vm_info
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = f"Failed to run container {name}: {e.stderr}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"name": name,
|
||||
"status": "error",
|
||||
"error": error_msg,
|
||||
"provider": "docker"
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = f"Error running VM {name}: {e}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"name": name,
|
||||
"status": "error",
|
||||
"error": error_msg,
|
||||
"provider": "docker"
|
||||
}
|
||||
|
||||
async def _wait_for_container_ready(self, container_name: str, timeout: int = 60) -> bool:
|
||||
"""Wait for the Docker container to be fully ready.
|
||||
|
||||
Args:
|
||||
container_name: Name of the Docker container to check
|
||||
timeout: Maximum time to wait in seconds (default: 60 seconds)
|
||||
|
||||
Returns:
|
||||
True if the container is running and ready
|
||||
"""
|
||||
logger.info(f"Waiting for container {container_name} to be ready...")
|
||||
|
||||
start_time = time.time()
|
||||
while time.time() - start_time < timeout:
|
||||
try:
|
||||
# Check if container is running
|
||||
vm_info = await self.get_vm(container_name)
|
||||
if vm_info["status"] == "running":
|
||||
logger.info(f"Container {container_name} is running")
|
||||
|
||||
# Additional check: try to connect to computer-server API
|
||||
# This is optional - we'll just wait a bit more for services to start
|
||||
await asyncio.sleep(5)
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.debug(f"Container {container_name} not ready yet: {e}")
|
||||
|
||||
await asyncio.sleep(2)
|
||||
|
||||
logger.warning(f"Container {container_name} did not become ready within {timeout} seconds")
|
||||
return False
|
||||
|
||||
async def stop_vm(self, name: str, storage: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Stop a running VM by stopping the Docker container."""
|
||||
try:
|
||||
logger.info(f"Stopping container {name}")
|
||||
|
||||
# Stop the container
|
||||
cmd = ["docker", "stop", name]
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
|
||||
|
||||
# Remove from running containers tracking
|
||||
if name in self._running_containers:
|
||||
del self._running_containers[name]
|
||||
|
||||
logger.info(f"Container {name} stopped successfully")
|
||||
|
||||
return {
|
||||
"name": name,
|
||||
"status": "stopped",
|
||||
"message": "Container stopped successfully",
|
||||
"provider": "docker"
|
||||
}
|
||||
|
||||
except subprocess.CalledProcessError as e:
|
||||
error_msg = f"Failed to stop container {name}: {e.stderr}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"name": name,
|
||||
"status": "error",
|
||||
"error": error_msg,
|
||||
"provider": "docker"
|
||||
}
|
||||
except Exception as e:
|
||||
error_msg = f"Error stopping VM {name}: {e}"
|
||||
logger.error(error_msg)
|
||||
return {
|
||||
"name": name,
|
||||
"status": "error",
|
||||
"error": error_msg,
|
||||
"provider": "docker"
|
||||
}
|
||||
|
||||
async def update_vm(self, name: str, update_opts: Dict[str, Any], storage: Optional[str] = None) -> Dict[str, Any]:
|
||||
"""Update VM configuration.
|
||||
|
||||
Note: Docker containers cannot be updated while running.
|
||||
This method will return an error suggesting to recreate the container.
|
||||
"""
|
||||
return {
|
||||
"name": name,
|
||||
"status": "error",
|
||||
"error": "Docker containers cannot be updated while running. Please stop and recreate the container with new options.",
|
||||
"provider": "docker"
|
||||
}
|
||||
|
||||
async def get_ip(self, name: str, storage: Optional[str] = None, retry_delay: int = 2) -> str:
|
||||
"""Get the IP address of a VM, waiting indefinitely until it's available.
|
||||
|
||||
Args:
|
||||
name: Name of the VM to get the IP for
|
||||
storage: Optional storage path override
|
||||
retry_delay: Delay between retries in seconds (default: 2)
|
||||
|
||||
Returns:
|
||||
IP address of the VM when it becomes available
|
||||
"""
|
||||
logger.info(f"Getting IP address for container {name}")
|
||||
|
||||
total_attempts = 0
|
||||
while True:
|
||||
total_attempts += 1
|
||||
|
||||
try:
|
||||
vm_info = await self.get_vm(name, storage)
|
||||
|
||||
if vm_info["status"] == "error":
|
||||
raise Exception(f"VM is in error state: {vm_info.get('error', 'Unknown error')}")
|
||||
|
||||
# TODO: for now, return localhost
|
||||
# it seems the docker container is not accessible from the host
|
||||
# on WSL2, unless you port forward? not sure
|
||||
if True:
|
||||
logger.warning("Overriding container IP with localhost")
|
||||
return "localhost"
|
||||
|
||||
# Check if we got a valid IP
|
||||
ip = vm_info.get("ip_address", None)
|
||||
if ip and ip != "unknown" and not ip.startswith("0.0.0.0"):
|
||||
logger.info(f"Got valid container IP address: {ip}")
|
||||
return ip
|
||||
|
||||
# For Docker containers, we can also use localhost if ports are mapped
|
||||
if vm_info["status"] == "running" and vm_info.get("ports"):
|
||||
logger.info(f"Container is running with port mappings, using localhost")
|
||||
return "127.0.0.1"
|
||||
|
||||
# Check the container status
|
||||
status = vm_info.get("status", "unknown")
|
||||
|
||||
if status == "stopped":
|
||||
logger.info(f"Container status is {status}, but still waiting for it to start")
|
||||
elif status != "running":
|
||||
logger.info(f"Container is not running yet (status: {status}). Waiting...")
|
||||
else:
|
||||
logger.info("Container is running but no valid IP address yet. Waiting...")
|
||||
|
||||
except Exception as e:
|
||||
logger.warning(f"Error getting container {name} IP: {e}, continuing to wait...")
|
||||
|
||||
# Wait before next retry
|
||||
await asyncio.sleep(retry_delay)
|
||||
|
||||
# Add progress log every 10 attempts
|
||||
if total_attempts % 10 == 0:
|
||||
logger.info(f"Still waiting for container {name} IP after {total_attempts} attempts...")
|
||||
|
||||
async def __aenter__(self):
|
||||
"""Async context manager entry."""
|
||||
logger.debug("Entering DockerProvider context")
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_val, exc_tb):
|
||||
"""Async context manager exit.
|
||||
|
||||
This method handles cleanup of running containers if needed.
|
||||
"""
|
||||
logger.debug(f"Exiting DockerProvider context, handling exceptions: {exc_type}")
|
||||
try:
|
||||
# Optionally stop running containers on context exit
|
||||
# For now, we'll leave containers running as they might be needed
|
||||
# Users can manually stop them if needed
|
||||
pass
|
||||
except Exception as e:
|
||||
logger.error(f"Error during DockerProvider cleanup: {e}")
|
||||
if exc_type is None:
|
||||
raise
|
||||
return False
|
||||
@@ -134,5 +134,29 @@ class VMProviderFactory:
|
||||
"pywinsandbox is required for WinSandboxProvider. "
|
||||
"Please install it with 'pip install -U git+https://github.com/karkason/pywinsandbox.git'"
|
||||
) from e
|
||||
elif provider_type == VMProviderType.DOCKER:
|
||||
try:
|
||||
from .docker import DockerProvider, HAS_DOCKER
|
||||
if not HAS_DOCKER:
|
||||
raise ImportError(
|
||||
"Docker is required for DockerProvider. "
|
||||
"Please install Docker and ensure it is running."
|
||||
)
|
||||
return DockerProvider(
|
||||
port=port,
|
||||
host=host,
|
||||
storage=storage,
|
||||
shared_path=shared_path,
|
||||
image=image or "trycua/cua-ubuntu:latest",
|
||||
verbose=verbose,
|
||||
ephemeral=ephemeral,
|
||||
vnc_port=noVNC_port
|
||||
)
|
||||
except ImportError as e:
|
||||
logger.error(f"Failed to import DockerProvider: {e}")
|
||||
raise ImportError(
|
||||
"Docker is required for DockerProvider. "
|
||||
"Please install Docker and ensure it is running."
|
||||
) from e
|
||||
else:
|
||||
raise ValueError(f"Unsupported provider type: {provider_type}")
|
||||
|
||||
Reference in New Issue
Block a user