Files
wehub-resource-sync 91e75e620b
CI: cua-driver distro-compat matrix / debian:12 (glibc 2.36) (push) Has been cancelled
CI: SPDX Headers / Check SPDX headers (warn-only) (push) Has been cancelled
CD: Docs MCP Server / build (linux/amd64) (push) Has been cancelled
CD: Docs MCP Server / build (linux/arm64) (push) Has been cancelled
CD: Docs MCP Server / merge (push) Has been cancelled
CI: cua-driver distro-compat matrix / Resolve release version (push) Has been cancelled
CI: cua-driver distro-compat matrix / fedora:41 (glibc 2.40) (push) Has been cancelled
CI: cua-driver distro-compat matrix / rockylinux:9 (glibc 2.34) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:22.04 (glibc 2.35) (push) Has been cancelled
CI: cua-driver distro-compat matrix / ubuntu:24.04 (glibc 2.39) (push) Has been cancelled
CI: cua-driver distro-compat matrix / Distro compat summary (push) Has been cancelled
CI: Rust Linux unit / Rust Linux unit and compile (push) Has been cancelled
CI: Rust Windows unit / Rust Windows unit and compile (push) Has been cancelled
CI: Nix Linux Rust source / Nix / compositor build (push) Has been cancelled
CI: Nix Linux Rust source / Nix / driver package (push) Has been cancelled
CI: Nix Linux Rust source / Nix / Rust unit tests (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:03:19 +08:00

794 lines
26 KiB
Bash
Executable File

#!/bin/bash
#
# setup-cua.sh
# Makes a Lume macOS VM fully cua-compatible
#
# This script runs ON the VM itself (not the host).
# It installs cua-computer-server, cua-agent, Playwright, noVNC (web-based VNC
# proxying to lume's built-in VNC on the host), supervisor, and configures the
# VM for headless CUA operation (auto-login, disable sleep, LaunchAgent).
#
# In Lume VMs, the display is provided by the Virtualization Framework and
# exposed through lume's built-in VNC server on the host. macOS Screen Sharing
# inside the VM has no framebuffer to capture. Therefore, noVNC/websockify
# inside the VM proxies to lume's VNC on the host (via the gateway IP).
#
# Usage:
# # Copy the script into the VM first, then run with a generous timeout:
# lume ssh <vm-name> "cat > /tmp/setup.sh && chmod +x /tmp/setup.sh" < setup-cua.sh
# lume ssh <vm-name> --timeout 600 "bash /tmp/setup.sh --yes"
#
# # Or copy and run directly inside the VM:
# ./setup-cua.sh [--yes] [--port PORT] [--host-vnc-port PORT]
#
# VNC: Lume auto-generates a random VNC port and password for each VM and
# delivers them to ~/.vnc.env inside the VM via SSH. The startup script and
# websockify read this file automatically — no manual --vnc-port or
# --vnc-password flags are needed.
#
# Accessing noVNC remotely (from outside the host):
# 1. SSH tunnel: ssh -L 6080:<vm-ip>:6080 user@host
# 2. Open: http://localhost:6080/vnc.html?host=localhost&port=6080
# The ?host=localhost&port=6080 params ensure the websocket connects through
# the tunnel instead of trying to reach the VM IP directly.
#
# Note: The default lume ssh timeout is 60s, which is too short for this script
# (Homebrew + Xcode CLT + pip installs can take 5-10 minutes). Use --timeout 600
# or --timeout 0 (unlimited) when running via lume ssh.
#
# Prerequisites:
# - macOS VM created via lume (with SSH enabled, user lume/lume)
#
# Host troubleshooting:
# If VMs fail to start with "Unable to access security information", the host's
# login keychain is likely locked (common after host reboot/crash on macOS Sequoia+).
# Fix: security unlock-keychain -p '<host-password>' ~/Library/Keychains/login.keychain-db
# Prevention: enable auto-login on the host so the keychain unlocks at boot.
#
set -e
# ============================================
# Configuration
# ============================================
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m'
AUTO_YES=false
CUA_SERVER_PORT=8443
VM_PASSWORD="${VM_PASSWORD:-lume}"
CUA_DIR="$HOME/.cua-server"
NOVNC_PORT=6080
HOST_VNC_PORT="${HOST_VNC_PORT:-}"
VNC_PASSWORD="${VNC_PASSWORD:-}"
# Parse arguments
while [ "$#" -gt 0 ]; do
case "$1" in
--yes|-y)
AUTO_YES=true
;;
--port)
CUA_SERVER_PORT="$2"
shift
;;
--host-vnc-port)
echo -e "${YELLOW}Note: --host-vnc-port is deprecated. Lume auto-generates VNC ports.${NC}"
HOST_VNC_PORT="$2"
shift
;;
--vnc-password)
echo -e "${YELLOW}Note: --vnc-password is deprecated. Lume auto-generates VNC passwords.${NC}"
VNC_PASSWORD="$2"
shift
;;
--help)
echo "Usage: $0 [OPTIONS]"
echo ""
echo "Options:"
echo " --yes, -y Non-interactive mode (accept all prompts)"
echo " --port PORT CUA computer server port (default: 8443)"
echo " --host-vnc-port PORT (deprecated) Lume auto-generates VNC ports"
echo " --vnc-password PWD (deprecated) Lume auto-generates VNC passwords"
echo " --help Show this help message"
echo ""
echo "Environment variables:"
echo " VM_PASSWORD VM user password (default: lume)"
echo " HOST_VNC_PORT (deprecated) Lume auto-generates VNC ports"
echo " VNC_PASSWORD (deprecated) Lume auto-generates VNC passwords"
exit 0
;;
*)
echo -e "${RED}Unknown option: $1${NC}"
exit 1
;;
esac
shift
done
CURRENT_USER="$(whoami)"
USER_HOME="$HOME"
# Auto-detect host gateway IP
GATEWAY_IP=$(route get default 2>/dev/null | grep gateway | awk '{print $2}')
if [ -z "$GATEWAY_IP" ]; then
GATEWAY_IP="192.168.64.1"
fi
# ============================================
# Helpers
# ============================================
print_phase() {
echo ""
echo -e "${BLUE}========================================${NC}"
echo -e "${BLUE} $1${NC}"
echo -e "${BLUE}========================================${NC}"
echo ""
}
confirm() {
local prompt="$1"
if [ "$AUTO_YES" = true ]; then
echo -e "${YELLOW}${prompt} [Y/n] ${NC}y (auto)"
return 0
fi
read -r -p "$(echo -e "${YELLOW}${prompt} [Y/n] ${NC}")" response
case "$response" in
[nN][oO]|[nN]) return 1 ;;
*) return 0 ;;
esac
}
ensure_brew() {
if [ -f /opt/homebrew/bin/brew ]; then
eval "$(/opt/homebrew/bin/brew shellenv)"
fi
}
# Create an askpass helper for non-interactive sudo
setup_sudo() {
if [ "$AUTO_YES" = true ]; then
ASKPASS_SCRIPT=$(mktemp)
printf '#!/bin/bash\necho "%s"\n' "$VM_PASSWORD" > "$ASKPASS_SCRIPT"
chmod +x "$ASKPASS_SCRIPT"
export SUDO_ASKPASS="$ASKPASS_SCRIPT"
sudo -A -v 2>/dev/null || true
fi
}
cleanup_sudo() {
if [ -n "${ASKPASS_SCRIPT:-}" ] && [ -f "$ASKPASS_SCRIPT" ]; then
rm -f "$ASKPASS_SCRIPT"
fi
}
trap cleanup_sudo EXIT
# ============================================
# Phase 1: Prerequisites (Homebrew + Python)
# ============================================
install_homebrew() {
print_phase "Phase 1: Homebrew"
if command -v brew &>/dev/null; then
echo -e "${GREEN}Homebrew already installed$(brew --version | head -1 | sed 's/Homebrew //' | sed 's/^/ (v/')${NC})"
ensure_brew
return 0
fi
if ! confirm "Install Homebrew?"; then
echo -e "${YELLOW}Skipping Homebrew. Later steps will fail.${NC}"
return 1
fi
setup_sudo
if [ "$AUTO_YES" = true ]; then
NONINTERACTIVE=1 SUDO_ASKPASS="$ASKPASS_SCRIPT" /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
else
echo -e "${YELLOW}Homebrew requires sudo access. Enter your password if prompted.${NC}"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
# Add brew to shell profile
if ! grep -q 'brew shellenv' "$HOME/.zprofile" 2>/dev/null; then
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$HOME/.zprofile"
fi
ensure_brew
echo -e "${GREEN}Homebrew installed${NC}"
}
install_python() {
print_phase "Phase 2: Python 3.13"
ensure_brew
if brew list python@3.13 &>/dev/null; then
echo -e "${GREEN}Python 3.13 already installed${NC}"
else
if ! confirm "Install Python 3.13 via Homebrew?"; then
echo -e "${YELLOW}Skipping Python 3.13.${NC}"
return 1
fi
brew install python@3.13
echo -e "${GREEN}Python 3.13 installed${NC}"
fi
export PATH="/opt/homebrew/opt/python@3.13/bin:$PATH"
}
install_system_deps() {
print_phase "Phase 3: System Dependencies"
ensure_brew
local deps_needed=()
for pkg in ffmpeg supervisor wget; do
if ! brew list "$pkg" &>/dev/null; then
deps_needed+=("$pkg")
fi
done
if [ ${#deps_needed[@]} -eq 0 ]; then
echo -e "${GREEN}All system dependencies already installed${NC}"
return 0
fi
if confirm "Install system dependencies (${deps_needed[*]})?"; then
brew install "${deps_needed[@]}"
echo -e "${GREEN}System dependencies installed${NC}"
fi
}
# ============================================
# Phase 4: CUA Server + Agent
# ============================================
setup_cua_server() {
print_phase "Phase 4: CUA Computer Server & Agent"
ensure_brew
export PATH="/opt/homebrew/opt/python@3.13/bin:$PATH"
if ! confirm "Install cua-computer-server and cua-agent?"; then
echo -e "${YELLOW}Skipping CUA server setup.${NC}"
return 1
fi
mkdir -p "$CUA_DIR"
# Create venv if needed
if [ ! -d "$CUA_DIR/venv" ]; then
echo "Creating Python 3.13 virtual environment..."
/opt/homebrew/opt/python@3.13/bin/python3.13 -m venv "$CUA_DIR/venv"
fi
echo "Installing cua-computer-server..."
source "$CUA_DIR/venv/bin/activate"
pip install --upgrade pip
pip install "cua-computer-server[vnc]"
echo "Installing cua-agent..."
pip install 'cua-agent[all]'
# Playwright + Firefox (non-blocking)
echo "Installing Playwright and Firefox browser..."
pip install playwright && playwright install firefox || \
echo -e "${YELLOW}Playwright install failed (non-blocking, BrowserTool may not work)${NC}"
# Install websockify for noVNC and vncdotool for VNC backend
echo "Installing websockify and vncdotool..."
pip install websockify vncdotool
deactivate
echo -e "${GREEN}CUA server and agent installed${NC}"
}
# ============================================
# Phase 5: noVNC + Websockify
# ============================================
setup_novnc() {
print_phase "Phase 5: noVNC (Web-based VNC)"
if ! confirm "Install noVNC and websockify (web VNC on port $NOVNC_PORT)?"; then
echo -e "${YELLOW}Skipping noVNC setup.${NC}"
return 0
fi
ensure_brew
local NOVNC_DIR="/usr/local/share/novnc"
setup_sudo
# Download noVNC
echo "Installing noVNC..."
cd /tmp
rm -rf noVNC-master noVNC-master.zip
wget -q https://github.com/trycua/noVNC/archive/refs/heads/master.zip -O noVNC-master.zip
unzip -q noVNC-master.zip
if [ "$AUTO_YES" = true ]; then
sudo -A rm -rf "$NOVNC_DIR"
sudo -A mkdir -p "$NOVNC_DIR"
sudo -A mv noVNC-master/* "$NOVNC_DIR/"
sudo -A ln -sf "$NOVNC_DIR/vnc.html" "$NOVNC_DIR/index.html"
sudo -A chmod -R 755 "$NOVNC_DIR"
else
sudo rm -rf "$NOVNC_DIR"
sudo mkdir -p "$NOVNC_DIR"
sudo mv noVNC-master/* "$NOVNC_DIR/"
sudo ln -sf "$NOVNC_DIR/vnc.html" "$NOVNC_DIR/index.html"
sudo chmod -R 755 "$NOVNC_DIR"
fi
rm -rf noVNC-master noVNC-master.zip
# Configure supervisor for websockify
# Proxies to lume's built-in VNC on the host via the gateway IP.
# The VNC port is read from ~/.vnc.env (written by lume via SSH).
local SUPERVISOR_CONF_DIR="/opt/homebrew/etc/supervisor.d"
local SUPERVISOR_LOG_DIR="/opt/homebrew/var/log/supervisor"
local VENV_PYTHON="$CUA_DIR/venv/bin/python3"
mkdir -p "$SUPERVISOR_CONF_DIR"
mkdir -p "$SUPERVISOR_LOG_DIR"
# Create a wrapper script that reads VNC config from ~/.vnc.env
local WEBSOCKIFY_WRAPPER="$CUA_DIR/start_websockify.sh"
cat > "$WEBSOCKIFY_WRAPPER" <<'WSEOF'
#!/bin/bash
# Read VNC port from ~/.vnc.env (written by lume via SSH after VM boot)
VNC_PORT=""
if [ -s "$HOME/.vnc.env" ]; then
eval "$(cat "$HOME/.vnc.env")"
fi
if [ -z "$VNC_PORT" ]; then
echo "No ~/.vnc.env found — websockify cannot start without VNC port" >&2
exit 1
fi
GATEWAY_IP=$(route get default 2>/dev/null | grep gateway | awk '{print $2}')
if [ -z "$GATEWAY_IP" ]; then
GATEWAY_IP="192.168.64.1"
fi
exec __VENV_PYTHON__ -m websockify --web __NOVNC_DIR__ __NOVNC_PORT__ "$GATEWAY_IP:$VNC_PORT"
WSEOF
sed -i '' "s|__VENV_PYTHON__|$VENV_PYTHON|g" "$WEBSOCKIFY_WRAPPER"
sed -i '' "s|__NOVNC_DIR__|$NOVNC_DIR|g" "$WEBSOCKIFY_WRAPPER"
sed -i '' "s|__NOVNC_PORT__|$NOVNC_PORT|g" "$WEBSOCKIFY_WRAPPER"
chmod +x "$WEBSOCKIFY_WRAPPER"
cat > "$SUPERVISOR_CONF_DIR/websockify.ini" <<EOF
[program:websockify]
command=/bin/bash $WEBSOCKIFY_WRAPPER
directory=$NOVNC_DIR
autostart=true
autorestart=true
startsecs=5
startretries=3
stopwaitsecs=10
redirect_stderr=true
stdout_logfile=$SUPERVISOR_LOG_DIR/websockify.log
stdout_logfile_maxbytes=10MB
stdout_logfile_backups=5
environment=HOME="$USER_HOME",PATH="/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin",DISPLAY=":0"
EOF
chmod 644 "$SUPERVISOR_CONF_DIR/websockify.ini"
echo -e "${GREEN}noVNC installed${NC}"
echo " websockify will proxy port $NOVNC_PORT -> lume VNC (port from ~/.vnc.env)"
}
# ============================================
# Phase 6: Startup Script + LaunchAgent
# ============================================
create_startup_script() {
print_phase "Phase 6: Startup Script & LaunchAgent"
if ! confirm "Create CUA server startup script and LaunchAgent?"; then
echo -e "${YELLOW}Skipping LaunchAgent setup.${NC}"
return 1
fi
local STARTUP_SCRIPT="$CUA_DIR/start_server.sh"
cat > "$STARTUP_SCRIPT" <<'SCRIPT_EOF'
#!/bin/bash
LOG_FILE="$HOME/.cua-server/server.log"
echo "=== CUA Server startup at $(date) ===" >> "$LOG_FILE"
# Wait for window server
sleep 10
cd "$HOME/.cua-server"
source venv/bin/activate
export DISPLAY=:0
# Auto-detect host gateway IP for VNC backend
GATEWAY_IP=$(route get default 2>/dev/null | grep gateway | awk '{print $2}')
if [ -z "$GATEWAY_IP" ]; then
GATEWAY_IP="192.168.64.1"
fi
# Read VNC config — check multiple sources in priority order:
# 1. Local vnc.env (written by host via lume ssh, or cached from VirtioFS)
# 2. VirtioFS lume-config mount (may be blocked by macOS TCC in LaunchAgents)
LUME_VNC_PORT=""
LUME_VNC_PASSWORD=""
LOCAL_VNC_ENV="$HOME/.vnc.env"
# Source 1: local vnc.env (most reliable — not subject to TCC restrictions)
if [ -s "$LOCAL_VNC_ENV" ]; then
eval "$(cat "$LOCAL_VNC_ENV")"
LUME_VNC_PORT="${VNC_PORT:-}"
LUME_VNC_PASSWORD="${VNC_PASSWORD:-}"
echo "Read VNC config from local vnc.env: port=$LUME_VNC_PORT" >> "$LOG_FILE"
fi
# Source 2: VirtioFS lume-config (fallback — may need sudo due to TCC)
if [ -z "$LUME_VNC_PORT" ]; then
LUME_CONFIG_MOUNT="/tmp/lume-config"
LUME_CONFIG="$LUME_CONFIG_MOUNT/vnc.env"
mkdir -p "$LUME_CONFIG_MOUNT" 2>/dev/null
if mount | grep -q "lume-config"; then
echo "lume-config already mounted" >> "$LOG_FILE"
elif mount_virtiofs lume-config "$LUME_CONFIG_MOUNT" 2>/dev/null; then
echo "Mounted lume-config VirtioFS share" >> "$LOG_FILE"
else
echo "lume-config VirtioFS not available" >> "$LOG_FILE"
fi
# Try reading vnc.env (wait up to 10s for it to appear)
for i in $(seq 1 10); do
# Try direct read first, then sudo (TCC may block direct access)
VNC_ENV_CONTENT=""
if VNC_ENV_CONTENT=$(cat "$LUME_CONFIG" 2>/dev/null) && [ -n "$VNC_ENV_CONTENT" ]; then
true
elif VNC_ENV_CONTENT=$(sudo -n cat "$LUME_CONFIG" 2>/dev/null) && [ -n "$VNC_ENV_CONTENT" ]; then
true
fi
if [ -n "$VNC_ENV_CONTENT" ]; then
eval "$VNC_ENV_CONTENT"
LUME_VNC_PORT="${VNC_PORT:-}"
LUME_VNC_PASSWORD="${VNC_PASSWORD:-}"
echo "Read VNC config from VirtioFS: port=$LUME_VNC_PORT" >> "$LOG_FILE"
# Cache locally for future restarts
echo "$VNC_ENV_CONTENT" > "$LOCAL_VNC_ENV"
break
fi
sleep 1
done
fi
if [ -z "$LUME_VNC_PORT" ]; then
echo "WARNING: No VNC config found in ~/.vnc.env or VirtioFS. VNC backend may not work." >> "$LOG_FILE"
echo "Lume should auto-deliver VNC config via SSH. Check that the VM was started with 'lume run'." >> "$LOG_FILE"
fi
# Set VNC backend env vars — prefer discovered values
export CUA_BACKEND=vnc
export CUA_VNC_HOST="$GATEWAY_IP"
export CUA_VNC_PORT="${LUME_VNC_PORT:-5900}"
export CUA_VNC_PASSWORD="${LUME_VNC_PASSWORD:-}"
# Update packages
echo "Updating cua-agent..." >> "$LOG_FILE"
pip install --upgrade --no-input "cua-agent[all]" >> "$LOG_FILE" 2>&1 || true
echo "Updating cua-computer-server..." >> "$LOG_FILE"
pip install --upgrade --no-input "cua-computer-server[vnc]" >> "$LOG_FILE" 2>&1 || true
# Ensure Playwright Firefox
echo "Ensuring Playwright Firefox..." >> "$LOG_FILE"
pip install --upgrade --no-input playwright >> "$LOG_FILE" 2>&1 || true
playwright install firefox >> "$LOG_FILE" 2>&1 || true
# Start server with VNC backend pointing at lume's VNC on the host
echo "Starting CUA computer server (vnc backend) on port __PORT__..." >> "$LOG_FILE"
echo " VNC target: $CUA_VNC_HOST:$CUA_VNC_PORT" >> "$LOG_FILE"
python -m computer_server --port __PORT__ >> "$LOG_FILE" 2>&1
SCRIPT_EOF
# Stamp the server port
sed -i '' "s/__PORT__/$CUA_SERVER_PORT/g" "$STARTUP_SCRIPT"
chmod +x "$STARTUP_SCRIPT"
# Create LaunchAgent
local SERVICE_NAME="com.trycua.computer_server"
local PLIST_PATH="$USER_HOME/Library/LaunchAgents/$SERVICE_NAME.plist"
mkdir -p "$USER_HOME/Library/LaunchAgents"
# Unload existing
launchctl unload "$PLIST_PATH" 2>/dev/null || true
cat > "$PLIST_PATH" <<EOF
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>$SERVICE_NAME</string>
<key>WorkingDirectory</key>
<string>$CUA_DIR</string>
<key>EnvironmentVariables</key>
<dict>
<key>PATH</key>
<string>/opt/homebrew/bin:/opt/homebrew/opt/python@3.13/bin:/usr/local/bin:/usr/bin:/bin:/usr/sbin:/sbin</string>
<key>HOME</key>
<string>$USER_HOME</string>
<key>DISPLAY</key>
<string>:0</string>
<key>CUA_BACKEND</key>
<string>vnc</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>/bin/bash</string>
<string>$CUA_DIR/start_server.sh</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/computer_server.log</string>
<key>StandardErrorPath</key>
<string>/tmp/computer_server.error.log</string>
<key>ProcessType</key>
<string>Interactive</string>
<key>SessionType</key>
<string>Aqua</string>
</dict>
</plist>
EOF
chmod 644 "$PLIST_PATH"
touch /tmp/computer_server.log /tmp/computer_server.error.log
echo -e "${GREEN}Startup script and LaunchAgent created${NC}"
echo "LaunchAgent will start CUA server on port $CUA_SERVER_PORT at login"
}
# ============================================
# Phase 7: System Configuration + Auto-Login
# ============================================
configure_system() {
print_phase "Phase 7: System Configuration"
if ! confirm "Disable screen lock, screensaver, sleep, and enable auto-login?"; then
echo -e "${YELLOW}Skipping system configuration.${NC}"
return 0
fi
setup_sudo
# Disable screensaver
defaults -currentHost write com.apple.screensaver idleTime 0
defaults write com.apple.screensaver askForPassword -int 0
defaults write com.apple.screensaver askForPasswordDelay -int 0
# Disable sleep
if [ "$AUTO_YES" = true ]; then
sudo -A pmset -a displaysleep 0 sleep 0 disksleep 0
else
sudo pmset -a displaysleep 0 sleep 0 disksleep 0
fi
# Enable full keyboard navigation (needed for accessibility automation)
defaults write NSGlobalDomain AppleKeyboardUIMode -int 3
# Configure auto-login (ensures GUI session + unlocked keychain on boot)
echo "Configuring auto-login for $CURRENT_USER..."
if [ "$AUTO_YES" = true ]; then
sudo -A defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "$CURRENT_USER"
else
sudo defaults write /Library/Preferences/com.apple.loginwindow autoLoginUser "$CURRENT_USER"
fi
# Create kcpassword file (XOR-encrypted password for auto-login)
python3 << PYEOF
password = "$VM_PASSWORD"
key = bytes.fromhex("7d895223d2bcddeaa3b91f")
padded_len = ((len(password) + 11) // 12) * 12
padded = password + "\x00" * (padded_len - len(password))
result = bytearray()
for i, char in enumerate(padded.encode("utf-8")):
result.append(char ^ key[i % len(key)])
with open("/tmp/kcpassword", "wb") as f:
f.write(result)
PYEOF
if [ "$AUTO_YES" = true ]; then
sudo -A mv /tmp/kcpassword /etc/kcpassword
sudo -A chown root:wheel /etc/kcpassword
sudo -A chmod 600 /etc/kcpassword
else
sudo mv /tmp/kcpassword /etc/kcpassword
sudo chown root:wheel /etc/kcpassword
sudo chmod 600 /etc/kcpassword
fi
echo -e "${GREEN}System configured (no sleep, no screensaver, full keyboard nav, auto-login)${NC}"
}
# ============================================
# Phase 8: Start Services
# ============================================
start_services() {
print_phase "Phase 8: Start Services"
ensure_brew
# Start supervisor (manages websockify)
if command -v brew &>/dev/null; then
echo "Starting supervisor..."
brew services start supervisor 2>/dev/null || true
sleep 3
if command -v supervisorctl &>/dev/null; then
supervisorctl reread 2>/dev/null || true
supervisorctl update 2>/dev/null || true
fi
fi
echo -e "${GREEN}Services started${NC}"
}
# ============================================
# Phase 9: Verification
# ============================================
verify() {
print_phase "Verification"
echo "System:"
echo " User: $CURRENT_USER"
echo " Gateway: $GATEWAY_IP"
sw_vers | sed 's/^/ /'
echo ""
echo "Installed:"
# Homebrew
if command -v brew &>/dev/null; then
echo -e " ${GREEN}${NC} Homebrew $(brew --version | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+')"
else
echo -e " ${RED}${NC} Homebrew"
fi
# Python
ensure_brew
if brew list python@3.13 &>/dev/null; then
echo -e " ${GREEN}${NC} Python 3.13"
else
echo -e " ${RED}${NC} Python 3.13"
fi
# CUA server
if [ -f "$CUA_DIR/venv/bin/python" ]; then
local server_ver
server_ver=$("$CUA_DIR/venv/bin/pip" show cua-computer-server 2>/dev/null | grep Version | awk '{print $2}')
echo -e " ${GREEN}${NC} cua-computer-server ${server_ver:-unknown}"
else
echo -e " ${RED}${NC} cua-computer-server"
fi
# Agent
if [ -f "$CUA_DIR/venv/bin/python" ]; then
local agent_ver
agent_ver=$("$CUA_DIR/venv/bin/pip" show cua-agent 2>/dev/null | grep Version | awk '{print $2}')
echo -e " ${GREEN}${NC} cua-agent ${agent_ver:-unknown}"
else
echo -e " ${RED}${NC} cua-agent"
fi
# Playwright
if "$CUA_DIR/venv/bin/python" -c "import playwright" 2>/dev/null; then
echo -e " ${GREEN}${NC} Playwright + Firefox"
else
echo -e " ${YELLOW}~${NC} Playwright (may not be installed)"
fi
# noVNC / websockify
if command -v supervisorctl &>/dev/null && supervisorctl status websockify 2>/dev/null | grep -q RUNNING; then
echo -e " ${GREEN}${NC} noVNC + websockify (port $NOVNC_PORT -> lume VNC)"
elif [ -d "/usr/local/share/novnc" ]; then
echo -e " ${YELLOW}~${NC} noVNC installed (websockify may not be running yet)"
else
echo -e " ${RED}${NC} noVNC"
fi
# Auto-login
if defaults read /Library/Preferences/com.apple.loginwindow autoLoginUser 2>/dev/null | grep -q "$CURRENT_USER"; then
echo -e " ${GREEN}${NC} Auto-login ($CURRENT_USER)"
else
echo -e " ${YELLOW}~${NC} Auto-login (may need reboot)"
fi
# LaunchAgent
local PLIST="$USER_HOME/Library/LaunchAgents/com.trycua.computer_server.plist"
if [ -f "$PLIST" ]; then
echo -e " ${GREEN}${NC} LaunchAgent (port $CUA_SERVER_PORT)"
else
echo -e " ${RED}${NC} LaunchAgent"
fi
echo ""
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} Setup Complete!${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo "Services:"
echo " CUA server: port $CUA_SERVER_PORT (starts at login via LaunchAgent)"
echo " noVNC: port $NOVNC_PORT -> lume VNC (auto-configured via ~/.vnc.env)"
echo ""
echo "VNC config is auto-delivered by lume. Just run:"
echo " lume run <vm-name>"
echo ""
echo "To start CUA server now (from the host):"
echo " lume ssh <vm-name> 'launchctl load ~/Library/LaunchAgents/com.trycua.computer_server.plist'"
echo ""
echo "To test from the host:"
echo " curl http://<vm-ip>:$CUA_SERVER_PORT/status"
echo ""
echo "To access noVNC from the host network:"
echo " http://<vm-ip>:$NOVNC_PORT/vnc.html"
echo ""
echo "To access noVNC remotely (from outside the host):"
echo " 1. SSH tunnel: ssh -L $NOVNC_PORT:<vm-ip>:$NOVNC_PORT user@host"
echo " 2. Open: http://localhost:$NOVNC_PORT/vnc.html?host=localhost&port=$NOVNC_PORT"
echo ""
echo "NOTE: A reboot is recommended for auto-login to take effect."
echo ""
}
# ============================================
# Main
# ============================================
echo -e "${BLUE}"
echo " ⠀⣀⣀⡀⠀⠀⠀⠀⢀⣀⣀⣀⡀⠘⠋⢉⠙⣷⠀⠀ ⠀"
echo " ⠀⠀⢀⣴⣿⡿⠋⣉⠁⣠⣾⣿⣿⣿⣿⡿⠿⣦⡈⠀⣿⡇⠃⠀"
echo " ⠀⠀⠀⣽⣿⣧⠀⠃⢰⣿⣿⡏⠙⣿⠿⢧⣀⣼⣷⠀⡿⠃⠀⠀"
echo " ⠀⠀⠀⠉⣿⣿⣦⠀⢿⣿⣿⣷⣾⡏⠀⠀⢹⣿⣿⠀⠀⠀⠀⠀⠀"
echo -e " ⠀⠀⠀⠀⠀⠉⠛⠁⠈⠿⣿⣿⣿⣷⣄⣠⡼⠟⠁${NC} ${BLUE}CUA Setup${NC}"
echo -e "${BLUE} Make a Lume VM cua-compatible${NC}"
echo ""
install_homebrew
install_python
install_system_deps
setup_cua_server
setup_novnc
create_startup_script
configure_system
start_services
verify