From 74055f496f2e2c421aa343ff6e3f7edc55526d80 Mon Sep 17 00:00:00 2001 From: Avi Fenesh Date: Tue, 4 Aug 2026 22:53:57 +0300 Subject: [PATCH] fix: support Artix and non-systemd installers --- .github/workflows/ci.yml | 9 ++ CHANGELOG.md | 4 + CONTRIBUTING.md | 1 + README.md | 8 +- install.sh | 183 +++++++++++++++++++++++++----- scripts/install_sh_test.sh | 82 +++++++++++++ tests/fixtures/os-release.artix | 4 + tests/fixtures/os-release.unknown | 3 + 8 files changed, 260 insertions(+), 34 deletions(-) create mode 100755 scripts/install_sh_test.sh create mode 100644 tests/fixtures/os-release.artix create mode 100644 tests/fixtures/os-release.unknown diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f30e319..76a7a52 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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 @@ -159,6 +167,7 @@ jobs: - check - clippy - test + - installer-shell - rustdoc-and-package - supply-chain - mcp-safety diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a6d447..c45af61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index a78643f..4aab31c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 . ``` diff --git a/README.md b/README.md index b2240f2..a4b64ce 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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. @@ -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. @@ -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. diff --git a/install.sh b/install.sh index 2bbf634..8e634b6 100755 --- a/install.sh +++ b/install.sh @@ -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" @@ -64,6 +65,7 @@ SKIP_YDOTOOL=0 SKIP_GNOME_EXT=0 SKIP_DOCTOR=0 FORCE_UNKNOWN_DISTRO=0 +PACKAGE_MANAGER_OVERRIDE="" usage() { cat </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" @@ -124,32 +165,50 @@ detect_distro() { 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. @@ -179,6 +238,37 @@ 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 @@ -186,7 +276,7 @@ install_system_deps() { 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 @@ -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 } # ----------------------------------------------------------------------------- @@ -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 @@ -507,4 +626,6 @@ main() { fi } -main "$@" +if [[ "${BASH_SOURCE[0]}" == "$0" ]]; then + main "$@" +fi diff --git a/scripts/install_sh_test.sh b/scripts/install_sh_test.sh new file mode 100755 index 0000000..d7380a0 --- /dev/null +++ b/scripts/install_sh_test.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +INSTALLER="${ROOT_DIR}/install.sh" +FIXTURE_DIR="${ROOT_DIR}/tests/fixtures" + +assert_eq() { + local actual="$1" expected="$2" + if [[ "${actual}" != "${expected}" ]]; then + printf 'expected %q, got %q\n' "${expected}" "${actual}" >&2 + return 1 + fi +} + +assert_contains() { + local actual="$1" expected="$2" + if [[ "${actual}" != *"${expected}"* ]]; then + printf 'expected output to contain %q\n%s\n' "${expected}" "${actual}" >&2 + return 1 + fi +} + +test_artix_selects_pacman() ( + export COMPUTER_USE_LINUX_OS_RELEASE_FILE="${FIXTURE_DIR}/os-release.artix" + export XDG_SESSION_TYPE=x11 + export XDG_CURRENT_DESKTOP=unknown + + # shellcheck source=../install.sh + source "${INSTALLER}" + package_manager_available() { [[ "$1" == "pacman" ]]; } + + detect_distro >/dev/null + assert_eq "${PKG_MANAGER}" "pacman" + assert_eq "${DISTRO_FAMILY}" "arch" +) + +test_unknown_distro_selects_only_available_manager() ( + export COMPUTER_USE_LINUX_OS_RELEASE_FILE="${FIXTURE_DIR}/os-release.unknown" + export XDG_SESSION_TYPE=x11 + export XDG_CURRENT_DESKTOP=unknown + + # shellcheck source=../install.sh + source "${INSTALLER}" + FORCE_UNKNOWN_DISTRO=1 + package_manager_available() { [[ "$1" == "dnf" ]]; } + + detect_distro >/dev/null + assert_eq "${PKG_MANAGER}" "dnf" + assert_eq "${DISTRO_FAMILY}" "fedora" +) + +test_non_systemd_host_gets_manual_guidance() ( + export XDG_RUNTIME_DIR="/run/user/test" + + # shellcheck source=../install.sh + source "${INSTALLER}" + systemd_user_manager_available() { return 1; } + ydotoold() { :; } + + local output + output="$(setup_ydotoold)" + assert_contains "${output}" "systemd --user is unavailable" + assert_contains "${output}" "configure your per-user supervisor to run" + assert_contains "${output}" "ydotoold --socket-path=/run/user/test/.ydotool_socket" + assert_contains "${output}" "do not run ydotoold as root" +) + +run_test() { + local name="$1" test_fn="$2" + if "${test_fn}"; then + printf 'ok - %s\n' "${name}" + else + printf 'not ok - %s\n' "${name}" >&2 + return 1 + fi +} + +run_test "Artix selects pacman" test_artix_selects_pacman +run_test "unknown distro selects its only supported manager" test_unknown_distro_selects_only_available_manager +run_test "non-systemd host gets manual ydotoold guidance" test_non_systemd_host_gets_manual_guidance diff --git a/tests/fixtures/os-release.artix b/tests/fixtures/os-release.artix new file mode 100644 index 0000000..755c731 --- /dev/null +++ b/tests/fixtures/os-release.artix @@ -0,0 +1,4 @@ +NAME="Artix Linux" +PRETTY_NAME="Artix Linux" +ID=artix +BUILD_ID=rolling diff --git a/tests/fixtures/os-release.unknown b/tests/fixtures/os-release.unknown new file mode 100644 index 0000000..24d0c07 --- /dev/null +++ b/tests/fixtures/os-release.unknown @@ -0,0 +1,3 @@ +NAME="Unknown Linux" +PRETTY_NAME="Unknown Linux" +ID=unknown