From a49cfcffe66ed4fde41166dda43a10fe1f2446da Mon Sep 17 00:00:00 2001 From: Haitao Pan Date: Thu, 18 Jun 2026 17:44:02 +0800 Subject: [PATCH] fix: support native macOS deployment --- scripts/setup-ai-workspace-all-in-one.sh | 81 ++++++++++++++++---- tests/setup-ai-workspace-all-in-one-test.sh | 83 +++++++++++++++++++++ 2 files changed, 148 insertions(+), 16 deletions(-) create mode 100755 tests/setup-ai-workspace-all-in-one-test.sh diff --git a/scripts/setup-ai-workspace-all-in-one.sh b/scripts/setup-ai-workspace-all-in-one.sh index 4a51c63..f24f268 100755 --- a/scripts/setup-ai-workspace-all-in-one.sh +++ b/scripts/setup-ai-workspace-all-in-one.sh @@ -247,6 +247,16 @@ detect_os() { esac } +run_as_root() { + if [ "$(id -u)" -eq 0 ]; then + "$@" + elif command -v sudo >/dev/null 2>&1; then + sudo "$@" + else + error "Root privileges are required to run: $*. Install sudo or rerun this script as root." + fi +} + acquire_deployment_lock() { if [ "${AI_WORKSPACE_DEPLOYMENT_LOCK_HELD:-false}" = "true" ]; then return @@ -323,17 +333,17 @@ install_prerequisites() { info "Installing required dependencies (git, ansible)..." if [ "$os" = "linux" ]; then if [ -f /etc/debian_version ]; then - sudo apt-get update -y + run_as_root apt-get update -y if grep -qi ubuntu /etc/os-release 2>/dev/null; then - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y git curl software-properties-common - sudo apt-add-repository --yes --update ppa:ansible/ansible - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y ansible + run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y git curl software-properties-common + run_as_root apt-add-repository --yes --update ppa:ansible/ansible + run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y ansible else - sudo DEBIAN_FRONTEND=noninteractive apt-get install -y git curl ansible + run_as_root env DEBIAN_FRONTEND=noninteractive apt-get install -y git curl ansible fi elif [ -f /etc/redhat-release ]; then - sudo yum install -y epel-release - sudo yum install -y git curl ansible + run_as_root yum install -y epel-release + run_as_root yum install -y git curl ansible else error "Unsupported Linux distribution. Please install git and ansible manually." fi @@ -806,9 +816,13 @@ resolve_unified_auth_token() { fi if [ -f "$AUTH_TOKEN_FILE" ]; then - info "Found existing unified auth token at $AUTH_TOKEN_FILE, reusing it." - tr -d '\r\n' < "$AUTH_TOKEN_FILE" - return + token="$(tr -d '\r\n' < "$AUTH_TOKEN_FILE")" + if [ -n "$token" ]; then + info "Found existing unified auth token at $AUTH_TOKEN_FILE, reusing it." + printf '%s' "$token" + return + fi + warn "Existing unified auth token file is empty; generating a replacement." fi info "No unified auth token provided. Generating a secure random token..." @@ -1352,6 +1366,23 @@ service_status_line() { printf ' %-28s : %-8s (%s)\n' "$label" "$state" "$detail" } +cli_status_line() { + local label=$1 + local command_name=$2 + local version + + if ! command -v "$command_name" >/dev/null 2>&1; then + printf ' %-28s : unavailable\n' "$label" + return + fi + + version="$("$command_name" --version 2>/dev/null | head -n 1 || true)" + if [ -z "$version" ]; then + version="unknown" + fi + printf ' %-28s : %s\n' "$label" "$version" +} + write_service_status() { local output_file=$1 shift @@ -1524,12 +1555,12 @@ uninstall_ai_workspace() { # System-wide services for svc in xworkspace-litellm xworkspace-qmd xworkspace-api xworkspace-console xworkspace-openclaw xworkmate-bridge xworkspace-ttyd vault postgresql xworkspace-hermes; do if systemctl is-active --quiet "$svc.service" 2>/dev/null; then - sudo systemctl stop "$svc.service" >/dev/null 2>&1 || true - sudo systemctl disable "$svc.service" >/dev/null 2>&1 || true - sudo rm -f "/etc/systemd/system/$svc.service" + run_as_root systemctl stop "$svc.service" >/dev/null 2>&1 || true + run_as_root systemctl disable "$svc.service" >/dev/null 2>&1 || true + run_as_root rm -f "/etc/systemd/system/$svc.service" fi done - sudo systemctl daemon-reload >/dev/null 2>&1 || true + run_as_root systemctl daemon-reload >/dev/null 2>&1 || true fi if command -v docker >/dev/null 2>&1; then @@ -1552,8 +1583,8 @@ uninstall_ai_workspace() { rm -rf "/tmp/ai-workspace-deploy" rm -rf "$HOME/.config/systemd/user/plus.svc.xworkspace."* if [ "$(id -u)" = "0" ] || sudo -n true 2>/dev/null; then - sudo rm -rf "/opt/ai-workspace" >/dev/null 2>&1 || true - sudo rm -rf "/etc/ai-workspace" >/dev/null 2>&1 || true + run_as_root rm -rf "/opt/ai-workspace" >/dev/null 2>&1 || true + run_as_root rm -rf "/etc/ai-workspace" >/dev/null 2>&1 || true fi fi fi @@ -1562,6 +1593,13 @@ uninstall_ai_workspace() { exit 0 } +if [ "${AI_WORKSPACE_BOOTSTRAP_LIB_ONLY:-false}" = "true" ]; then + if [ "${BASH_SOURCE[0]}" != "$0" ]; then + return 0 + fi + exit 0 +fi + info "Starting AI Workspace All-in-One Bootstrap..." # 1. Install prerequisites (git, curl, ansible) if missing @@ -1841,6 +1879,7 @@ ANSIBLE_EXTRA_VARS+=("-e" "agent_skills_quality_gate_fail_on_error=false") if [ "$(detect_os)" = "darwin" ]; then info "Disabling global privilege escalation for macOS..." + DARWIN_SERVICE_PATH="$HOME/.nix-profile/bin:$HOME/.local/bin:$HOME/.npm-global/bin:$HOME/bin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:/usr/local/sbin:/usr/bin:/bin" ANSIBLE_EXTRA_VARS+=("-e" "ansible_become=false") ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_user=$(id -un)") ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_home=$HOME") @@ -1848,6 +1887,16 @@ if [ "$(detect_os)" = "darwin" ]; then ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_config_dir=$HOME/.config/ai-workspace") ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_scripts_dir=$HOME/xworkspace/scripts") ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_repo_dir=$HOME/xworkspace-console") + ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_group=staff") + ANSIBLE_EXTRA_VARS+=("-e" "xworkspace_console_ttyd_binary_path=$(command -v ttyd)") + ANSIBLE_EXTRA_VARS+=("-e" "agent_skills_user=$(id -un)") + ANSIBLE_EXTRA_VARS+=("-e" "agent_skills_group=staff") + ANSIBLE_EXTRA_VARS+=("-e" "agent_skills_home=$HOME") + ANSIBLE_EXTRA_VARS+=("-e" "gateway_openclaw_service_user=$(id -un)") + ANSIBLE_EXTRA_VARS+=("-e" "gateway_openclaw_service_group=staff") + ANSIBLE_EXTRA_VARS+=("-e" "gateway_openclaw_home=$HOME") + ANSIBLE_EXTRA_VARS+=("-e" "gateway_openclaw_compile_cache_dir=$HOME/.cache/openclaw-compile-cache") + ANSIBLE_EXTRA_VARS+=("-e" "gateway_openclaw_service_path=$DARWIN_SERVICE_PATH") fi # Export environment fallbacks for roles/scripts that read environment directly. diff --git a/tests/setup-ai-workspace-all-in-one-test.sh b/tests/setup-ai-workspace-all-in-one-test.sh new file mode 100755 index 0000000..c276104 --- /dev/null +++ b/tests/setup-ai-workspace-all-in-one-test.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +BOOTSTRAP="$SCRIPT_DIR/../scripts/setup-ai-workspace-all-in-one.sh" + +export AI_WORKSPACE_BOOTSTRAP_LIB_ONLY=true +# shellcheck source=/dev/null +source "$BOOTSTRAP" + +fail() { + printf 'not ok - %s\n' "$1" >&2 + exit 1 +} + +test_root_does_not_require_sudo() ( + # shellcheck disable=SC2329 + id() { + [ "${1:-}" = "-u" ] && printf '0\n' + } + # shellcheck disable=SC2329 + command() { + if [ "${1:-}" = "-v" ] && [ "${2:-}" = "sudo" ]; then + return 1 + fi + builtin command "$@" + } + probe_file="$(mktemp)" + # The positional parameter is intentionally expanded by the child shell. + # shellcheck disable=SC2016 + run_as_root sh -c 'printf root > "$1"' sh "$probe_file" + [ "$(cat "$probe_file")" = "root" ] || fail "root command was not executed directly" + rm -f "$probe_file" +) + +test_non_root_uses_sudo() ( + # shellcheck disable=SC2329 + id() { + [ "${1:-}" = "-u" ] && printf '1000\n' + } + # shellcheck disable=SC2329 + sudo() { + printf '%s\n' "$*" > "$sudo_log" + } + # shellcheck disable=SC2329 + command() { + if [ "${1:-}" = "-v" ] && [ "${2:-}" = "sudo" ]; then + return 0 + fi + builtin command "$@" + } + sudo_log="$(mktemp)" + run_as_root apt-get update -y + [ "$(cat "$sudo_log")" = "apt-get update -y" ] || fail "non-root command did not use sudo" + rm -f "$sudo_log" +) + +test_non_root_without_sudo_fails_cleanly() ( + # shellcheck disable=SC2329 + id() { + [ "${1:-}" = "-u" ] && printf '1000\n' + } + # shellcheck disable=SC2329 + command() { + if [ "${1:-}" = "-v" ] && [ "${2:-}" = "sudo" ]; then + return 1 + fi + builtin command "$@" + } + run_as_root apt-get update -y +) + +test_root_does_not_require_sudo +printf 'ok - root execution does not require sudo\n' +test_non_root_uses_sudo +printf 'ok - non-root execution uses sudo\n' +set +e +privilege_error="$(test_non_root_without_sudo_fails_cleanly 2>&1)" +privilege_status=$? +set -e +[ "$privilege_status" -ne 0 ] || fail "non-root execution without sudo unexpectedly succeeded" +printf '%s' "$privilege_error" | grep -q "Root privileges are required" || fail "missing sudo error was not actionable" +printf 'ok - missing sudo reports a privilege error\n'