Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,14 @@ jobs:
- run: sudo apt-get update && sudo apt-get install -y libdbus-1-dev pkg-config
- run: cargo test --locked --target ${{ matrix.target }} --no-fail-fast

installer-shell:
name: install.sh regressions
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: bash -n install.sh scripts/install_sh_test.sh
- run: scripts/install_sh_test.sh

rustdoc-and-package:
name: rustdoc and cargo package
runs-on: ubuntu-latest
Expand Down Expand Up @@ -159,6 +167,7 @@ jobs:
- check
- clippy
- test
- installer-shell
- rustdoc-and-package
- supply-chain
- mcp-safety
Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]

### Fixed
- `install.sh` now recognizes Artix as pacman-based, chooses an explicitly
requested or unambiguous package manager for unknown distros, treats
ydotool as an optional fallback, and skips automatic ydotoold setup when a
systemd user manager is unavailable.
- Native X11 coordinate clicks now use one supervised
`xdotool mousemove -- X Y click --repeat N BUTTON` command after the
absolute pointer and eligible portal paths, with `ydotool` fallback only
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ cargo fmt --all -- --check
cargo check --locked --all-targets
cargo clippy --locked --all-targets -- -D warnings
cargo test --locked --no-fail-fast
scripts/install_sh_test.sh
scripts/mcp_safety_check.py
agnix .
```
Expand Down
8 changes: 5 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -120,7 +120,7 @@ COSMIC users do not need a second package or a separate helper install when usin

### Option A — `./install.sh` from a clone

Installs system packages on Debian/Ubuntu, Fedora/RHEL-like, or Arch-like distros; installs Rust if needed; builds both release binaries; installs them to `~/.local/bin`; enables `ydotoold` as a user service; enables GNOME AT-SPI settings when running under GNOME; and installs the bundled GNOME Shell extension on GNOME Wayland.
Installs system packages on Debian/Ubuntu, Fedora/RHEL-like, Arch-like, or Artix systems; installs Rust if needed; builds both release binaries; installs them to `~/.local/bin`; configures `ydotoold` as a systemd user service when available; enables GNOME AT-SPI settings when running under GNOME; and installs the bundled GNOME Shell extension on GNOME Wayland.

```bash
git clone https://github.com/agent-sh/computer-use-linux
Expand All @@ -130,6 +130,8 @@ cd computer-use-linux
computer-use-linux doctor | jq .readiness
```

`ydotool` is an optional fallback. If it is unavailable from the configured repositories, the installer continues so the portal, direct uinput, or X11 xdotool backends can still satisfy `doctor`. On non-systemd hosts, automatic `ydotoold` service setup is skipped and the installer prints a command suitable for a per-user supervisor. For an unrecognized distro, pass `--package-manager apt|dnf|pacman`; `--force-unknown-distro` auto-selects only when exactly one of those managers is available.

### Option B — `cargo install` (Rust binaries, no system setup)

Installs the Rust binaries from crates.io. You still handle the system-level pieces yourself: AT-SPI, desktop portals, the optional `ydotoold` fallback, and the GNOME extension if you need the GNOME Wayland exact-focus backend.
Expand Down Expand Up @@ -370,7 +372,7 @@ files.

Computer-use tooling is, by definition, a privilege-escalation surface. The threat model:

- **`ydotoold` runs as a per-user systemd service** with read/write access to `/dev/uinput`. Any process that can connect to its socket (`/run/user/$UID/.ydotool_socket`, mode `0600` by default) can synthesize arbitrary input — keypresses, clicks, anything. Keep the socket in the user runtime dir (the default), not in `/tmp` or any world-readable location. Do not run `ydotoold` as a system service.
- **`ydotoold` runs as a per-user service** with read/write access to `/dev/uinput`. `install.sh` automates this for systemd user sessions and prints manual supervisor guidance elsewhere. Any process that can connect to its socket (`/run/user/$UID/.ydotool_socket`, mode `0600` by default) can synthesize arbitrary input — keypresses, clicks, anything. Keep the socket in the user runtime dir (the default), not in `/tmp` or any world-readable location. Do not run `ydotoold` as root or as a system service.
- **The screencast portal asks for permission once per session.** Granting it lets the calling MCP host capture the screen for the rest of the session. If you don't want that, decline the portal dialog and use `get_app_state` with `include_screenshot: false`.
- **AT-SPI exposes window contents to any client on your session bus.** Enabling the AT-SPI bridge (`setup_accessibility`) is a prerequisite for this binary; it's also what screen readers use, and it shares the same trust boundary.
- **The GNOME Shell extension** is loaded only into your user's GNOME Shell, runs in the Shell's JS sandbox, and exposes a single DBus interface on the user session bus. It does not request any extra permissions.
Expand All @@ -385,7 +387,7 @@ If you're running this on a shared workstation, set `ydotoold`'s socket permissi

- **`accessibility.at_spi_bus.ok = false`** — AT-SPI registry isn't running or the toolkit bridge is off. Fix: `computer-use-linux setup` (or call the `setup_accessibility` MCP tool). Restart the apps you want to drive.
- **`windowing.gnome_shell_introspect.ok = false` and `gnome_shell_extension_dbus.ok = false`** — GNOME blocks introspection and the extension isn't installed. Fix: `computer-use-linux setup-window-targeting`, then log out and log back in.
- **`input.ydotool_socket.ok = false` while ydotool is the selected fallback** — daemon isn't running. Fix: `systemctl --user enable --now ydotoold`. If the unit doesn't exist, install the `ydotool` package and rerun `./install.sh` (or copy the unit from `systemd/ydotoold.service` in this repo).
- **`input.ydotool_socket.ok = false` while ydotool is the selected fallback** — daemon isn't running. On systemd, run `systemctl --user enable --now ydotoold`. On other init systems, rerun `./install.sh` and configure your per-user supervisor with the command it prints. If `ydotool` is not packaged for your distro, use another input backend or install a compatible ydotool release manually.
- **`input.ydotool.ok = false` with an unsupported CLI message** — install ydotool 1.0.3 or newer. A running daemon or socket alone is not enough; `doctor` verifies the raw key, wheel, stdin typing, and absolute-movement command family before advertising the backend.
- **`input.uinput.ok = false`** — `/dev/uinput` isn't accessible to your user. Fix: add yourself to the `input` group (`sudo usermod -aG input $USER`) and re-login. On distros that ship `uinput` as a kernel module without auto-loading it, add `uinput` to `/etc/modules-load.d/`.
- **Portal calls hang or time out** — `xdg-desktop-portal` or its backend (`-gnome`, `-gtk`, `-kde`, `-wlr`) crashed. Fix: check `journalctl --user -u xdg-desktop-portal -u xdg-desktop-portal-gnome --since '5 min ago'` and restart the relevant unit.
Expand Down
183 changes: 152 additions & 31 deletions install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ IFS=$'\n\t'
# -----------------------------------------------------------------------------

SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" &>/dev/null && pwd)"
OS_RELEASE_FILE="${COMPUTER_USE_LINUX_OS_RELEASE_FILE:-/etc/os-release}"
BIN_NAME="computer-use-linux"
COSMIC_HELPER_NAME="computer-use-linux-cosmic"
INSTALL_DIR="${HOME}/.local/bin"
Expand Down Expand Up @@ -64,6 +65,7 @@ SKIP_YDOTOOL=0
SKIP_GNOME_EXT=0
SKIP_DOCTOR=0
FORCE_UNKNOWN_DISTRO=0
PACKAGE_MANAGER_OVERRIDE=""

usage() {
cat <<EOF
Expand All @@ -77,7 +79,7 @@ Steps (run in order, each idempotent):
3. Install rustup toolchain
4. cargo build --release → ~/.local/bin/${BIN_NAME} and ${COSMIC_HELPER_NAME}
5. Enable AT-SPI toolkit accessibility (GNOME)
6. Install + enable ydotoold systemd --user service
6. Install + enable ydotoold systemd --user service when available
7. Pack/install/enable GNOME Shell extension (Wayland + GNOME)
8. Run \`${BIN_NAME} doctor\` and report readiness

Expand All @@ -86,10 +88,11 @@ Flags:
--skip-rust skip rustup install
--skip-build skip cargo build (assumes target/release/${BIN_NAME} and ${COSMIC_HELPER_NAME} exist)
--skip-atspi skip toolkit-accessibility gsetting
--skip-ydotool skip ydotoold systemd unit
--skip-ydotool skip ydotoold user-service setup
--skip-gnome-extension skip GNOME Shell extension install
--skip-doctor skip the final readiness check
--force-unknown-distro treat unrecognised distros as Debian-family (apt)
--force-unknown-distro use the one supported package manager found on PATH
--package-manager NAME force apt, dnf, or pacman for system packages
-h, --help show this help and exit
EOF
}
Expand All @@ -104,52 +107,108 @@ while [[ $# -gt 0 ]]; do
--skip-gnome-extension) SKIP_GNOME_EXT=1 ;;
--skip-doctor) SKIP_DOCTOR=1 ;;
--force-unknown-distro) FORCE_UNKNOWN_DISTRO=1 ;;
--package-manager)
[[ $# -ge 2 ]] || die "--package-manager requires apt, dnf, or pacman"
PACKAGE_MANAGER_OVERRIDE="$2"
shift
;;
--package-manager=*) PACKAGE_MANAGER_OVERRIDE="${1#*=}" ;;
-h|--help) usage; exit 0 ;;
*) usage; die "unknown flag: $1" ;;
esac
shift
done

case "${PACKAGE_MANAGER_OVERRIDE}" in
""|apt|dnf|pacman) ;;
*) die "unsupported package manager '${PACKAGE_MANAGER_OVERRIDE}' (expected apt, dnf, or pacman)" ;;
esac

# -----------------------------------------------------------------------------
# Step 1: distro + display server detection
# -----------------------------------------------------------------------------

DISTRO_FAMILY=""
PKG_MANAGER=""

set_package_manager() {
case "$1" in
apt) DISTRO_FAMILY="debian"; PKG_MANAGER="apt" ;;
dnf) DISTRO_FAMILY="fedora"; PKG_MANAGER="dnf" ;;
pacman) DISTRO_FAMILY="arch"; PKG_MANAGER="pacman" ;;
*) return 1 ;;
esac
}

package_manager_available() {
case "$1" in
apt) command -v apt-get >/dev/null 2>&1 ;;
dnf) command -v dnf >/dev/null 2>&1 ;;
pacman) command -v pacman >/dev/null 2>&1 ;;
*) return 1 ;;
esac
}

available_package_managers() {
local manager
for manager in apt dnf pacman; do
if package_manager_available "${manager}"; then
printf '%s\n' "${manager}"
fi
done
}

detect_distro() {
log_section "Step 1/9 — detect environment"

if [[ "$(uname -s)" != "Linux" ]]; then
die "this script only supports Linux (got $(uname -s)). macOS/*BSD are not supported."
fi

if [[ ! -r /etc/os-release ]]; then
die "/etc/os-release missing — cannot detect distro."
if [[ ! -r "${OS_RELEASE_FILE}" ]]; then
die "${OS_RELEASE_FILE} missing — cannot detect distro."
fi

local ID="" ID_LIKE="" PRETTY_NAME=""
# shellcheck disable=SC1091
. /etc/os-release
. "${OS_RELEASE_FILE}"
local id_like="${ID_LIKE:-} ${ID:-}"

case " ${id_like} " in
*" debian "*|*" ubuntu "*)
DISTRO_FAMILY="debian"; PKG_MANAGER="apt" ;;
*" fedora "*|*" rhel "*|*" centos "*)
DISTRO_FAMILY="fedora"; PKG_MANAGER="dnf" ;;
*" arch "*|*" archlinux "*|*" manjaro "*|*" endeavouros "*)
DISTRO_FAMILY="arch"; PKG_MANAGER="pacman" ;;
*)
if [[ ${FORCE_UNKNOWN_DISTRO} -eq 1 ]]; then
log_warn "unknown distro '${ID:-?}' — forcing debian/apt path"
DISTRO_FAMILY="debian"; PKG_MANAGER="apt"
else
log_fail "unsupported distro: ${ID:-unknown} (${PRETTY_NAME:-?})"
log_info "supported families: debian/ubuntu, fedora, arch"
log_info "re-run with --force-unknown-distro to attempt apt-based install"
exit 1
fi ;;
esac
if [[ -n "${PACKAGE_MANAGER_OVERRIDE}" ]]; then
set_package_manager "${PACKAGE_MANAGER_OVERRIDE}"
log_warn "using requested package manager: ${PACKAGE_MANAGER_OVERRIDE}"
else
case " ${id_like} " in
*" debian "*|*" ubuntu "*)
set_package_manager apt ;;
*" fedora "*|*" rhel "*|*" centos "*)
set_package_manager dnf ;;
*" arch "*|*" archlinux "*|*" manjaro "*|*" endeavouros "*|*" artix "*|*" artixlinux "*)
set_package_manager pacman ;;
*)
if [[ ${FORCE_UNKNOWN_DISTRO} -eq 1 ]]; then
local available=()
mapfile -t available < <(available_package_managers)
if [[ ${#available[@]} -ne 1 ]]; then
log_fail "cannot choose a package manager for '${ID:-unknown}'"
log_info "found: ${available[*]:-none}; pass --package-manager apt|dnf|pacman"
return 1
fi
set_package_manager "${available[0]}"
log_warn "unknown distro '${ID:-?}' — using detected ${PKG_MANAGER}"
else
log_fail "unsupported distro: ${ID:-unknown} (${PRETTY_NAME:-?})"
log_info "supported families: debian/ubuntu, fedora, arch/artix"
log_info "re-run with --force-unknown-distro or --package-manager apt|dnf|pacman"
return 1
fi ;;
esac
fi

if ! package_manager_available "${PKG_MANAGER}"; then
log_fail "${PKG_MANAGER} was selected but its command is not on PATH"
return 1
fi
log_ok "distro family: ${DISTRO_FAMILY} (pkg manager: ${PKG_MANAGER})"

# Display server.
Expand Down Expand Up @@ -179,14 +238,45 @@ detect_distro() {
# Step 2: system package install
# -----------------------------------------------------------------------------

ydotool_package_available() {
case "${PKG_MANAGER}" in
apt) apt-cache show ydotool >/dev/null 2>&1 ;;
dnf) dnf info -q ydotool >/dev/null 2>&1 ;;
pacman) pacman -Si ydotool >/dev/null 2>&1 ;;
*) return 1 ;;
esac
}

install_optional_ydotool() {
if command -v ydotool >/dev/null 2>&1 && command -v ydotoold >/dev/null 2>&1; then
log_ok "optional ydotool fallback already installed"
return 0
fi
if ! ydotool_package_available; then
log_warn "optional ydotool package is unavailable from configured ${PKG_MANAGER} repositories"
log_info "the portal, direct uinput, or X11 xdotool backends may still satisfy doctor"
return 0
fi

log_info "installing optional ydotool fallback"
case "${PKG_MANAGER}" in
apt) sudo apt-get install -y ydotool ;;
dnf) sudo dnf install -y ydotool ;;
pacman) sudo pacman -S --needed --noconfirm ydotool ;;
esac || {
log_warn "optional ydotool install failed — continuing with the other input backends"
return 0
}
}

install_system_deps() {
log_section "Step 2/9 — system packages"
if [[ ${SKIP_SYSTEM_DEPS} -eq 1 ]]; then log_skip "--skip-system-deps"; return 0; fi

local desktop="${XDG_CURRENT_DESKTOP:-}"
case "${PKG_MANAGER}" in
apt)
local pkgs=(build-essential pkg-config libdbus-1-dev libssl-dev curl ydotool at-spi2-core)
local pkgs=(build-essential pkg-config libdbus-1-dev libssl-dev curl at-spi2-core)
sudo apt-get update -qq
if [[ "${desktop}" == *GNOME* ]] && ! command -v gnome-extensions >/dev/null 2>&1; then
if apt-cache show gnome-shell >/dev/null 2>&1; then
Expand All @@ -199,17 +289,18 @@ install_system_deps() {
sudo apt-get install -y "${pkgs[@]}" || { log_fail "apt-get install failed"; return 1; }
;;
dnf)
local pkgs=(gcc pkgconfig dbus-devel openssl-devel curl ydotool at-spi2-core)
local pkgs=(gcc pkgconfig dbus-devel openssl-devel curl at-spi2-core)
log_info "sudo dnf install -y ${pkgs[*]}"
sudo dnf install -y "${pkgs[@]}" || { log_fail "dnf install failed"; return 1; }
;;
pacman)
local pkgs=(base-devel pkgconf dbus openssl curl ydotool at-spi2-core)
local pkgs=(base-devel pkgconf dbus openssl curl at-spi2-core)
log_info "sudo pacman -S --needed --noconfirm ${pkgs[*]}"
sudo pacman -S --needed --noconfirm "${pkgs[@]}" || { log_fail "pacman install failed"; return 1; }
;;
esac
log_ok "system packages installed"
log_ok "required system packages installed"
install_optional_ydotool
}

# -----------------------------------------------------------------------------
Expand Down Expand Up @@ -306,14 +397,42 @@ enable_atspi() {
}

# -----------------------------------------------------------------------------
# Step 6: ydotoold systemd --user service
# Step 6: ydotoold user service
# -----------------------------------------------------------------------------

systemd_user_manager_available() {
command -v systemctl >/dev/null 2>&1 &&
systemctl --user show-environment >/dev/null 2>&1
}

show_manual_ydotoold_guidance() {
local ydotoold_path runtime_dir user_gid
ydotoold_path="$(command -v ydotoold 2>/dev/null || true)"
runtime_dir="${XDG_RUNTIME_DIR:-/run/user/${UID}}"
user_gid="$(id -g)"
if [[ -z "${ydotoold_path}" ]]; then
log_info "ydotool is optional; install it only if doctor needs that fallback"
return 0
fi
log_info "configure your per-user supervisor to run:"
log_info " ${ydotoold_path} --socket-path=${runtime_dir}/.ydotool_socket --socket-own=${UID}:${user_gid}"
log_info "do not run ydotoold as root or expose its socket to other users"
}

setup_ydotoold() {
log_section "Step 6/9 — ydotoold user service"
if [[ ${SKIP_YDOTOOL} -eq 1 ]]; then log_skip "--skip-ydotool"; return 0; fi

command -v ydotoold >/dev/null 2>&1 || { log_fail "ydotoold not found in PATH (install via system deps step)"; return 1; }
if ! systemd_user_manager_available; then
log_warn "systemd --user is unavailable — skipping automatic ydotoold service setup"
show_manual_ydotoold_guidance
return 0
fi

if ! command -v ydotoold >/dev/null 2>&1; then
log_warn "optional ydotoold fallback is not installed — skipping its user service"
return 0
fi

# /dev/uinput permissions check.
if [[ ! -e /dev/uinput ]]; then
Expand Down Expand Up @@ -507,4 +626,6 @@ main() {
fi
}

main "$@"
if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then
main "$@"
fi
Loading
Loading