#!/bin/bash set -euo pipefail ############################################### # TacticalRMM / Mesh Agent macOS Helper Script # All output/messages are in English. # Requires: curl, unzip, sqlite3, xattr, launchctl, installer ############################################### # ----------------------- # Usage examples (how to call the script) # ----------------------- # sudo /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/Mattchis/MacRMM-Script/main/rmmagent-mac.sh) install 'APIURL' 'ClientID' 'SiteID' 'AuthKey' 'AgentType'" # sudo /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/Mattchis/MacRMM-Script/main/rmmagent-mac.sh) install 'https://api.tld.com/' '5' '9' 'xxxxxxxxxx' 'workstation'" # sudo /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/Mattchis/MacRMM-Script/main/rmmagent-mac.sh) auto_install" # sudo /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/Mattchis/MacRMM-Script/main/rmmagent-mac.sh) interactive_install" # sudo /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/Mattchis/MacRMM-Script/main/rmmagent-mac.sh) enablepermissions" # sudo /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/Mattchis/MacRMM-Script/main/rmmagent-mac.sh) sequoiafix" # sudo /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/Mattchis/MacRMM-Script/main/rmmagent-mac.sh) update" # sudo /bin/bash -c "$(curl -sSL https://raw.githubusercontent.com/Mattchis/MacRMM-Script/main/rmmagent-mac.sh) uninstall" # ----------------------- # Global configuration # ----------------------- # Toggle debug output (true|false) DEBUG=${DEBUG:-false} ############################################### # Preconfigured variables for auto_install (edit as needed) API_URL="https://api.tld.com/" CUSTOMER_ID="5" GROUP_ID="9" AGENT_KEY="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" AGENT_TYPE="workstation" # server|workstation ############################################### # Go download URLs GO_URL_AMD64="https://go.dev/dl/go1.24.4.darwin-amd64.pkg" GO_URL_ARM64="https://go.dev/dl/go1.24.4.darwin-arm64.pkg" # Temp working dir (auto-cleaned) WORKDIR="$(mktemp -d /tmp/trmm.XXXXXX)" cleanup() { rm -rf "$WORKDIR" 2>/dev/null || true; } trap cleanup EXIT # ----------------------- # UI helpers # ----------------------- banner() { local message="${1:-}" echo "" echo "###############################################" echo "# #" printf "# %-43s#\n" "$message" echo "# #" echo "###############################################" echo "" } note() { echo "[*] $*"; } ok() { echo "[OK] $*"; } err() { echo "[!!] $*" >&2; } wait_with_progress() { local seconds="${1:-30}" local message="${2:-Waiting for agent to initialize}" echo "$message..." for (( i=seconds; i>=1; i-- )); do printf "\r%s... %d seconds remaining" "$message" "$i" sleep 1 done printf "\r%s... Complete! \n" "$message" echo "" } # ----------------------- # System checks/helpers # ----------------------- check_root() { banner "Privilege Verification" if [[ "$(id -u)" -ne 0 ]]; then err "This script must be run with root privileges (sudo)." exit 1 fi ok "Root privileges confirmed." } require_cmd() { local cmd="$1" if ! command -v "$cmd" >/dev/null 2>&1; then err "Required command not found: $cmd" exit 1 fi } check_dependencies() { banner "Dependency Check" local deps=(curl unzip sqlite3 xattr launchctl installer codesign awk xxd tee) for c in "${deps[@]}"; do require_cmd "$c" done ok "All required commands are available." } get_macos_version() { sw_vers -productVersion 2>/dev/null | awk -F. '{print $1 "." $2}' } is_sequoia() { # macOS 15.x = Sequoia local v v="$(sw_vers -productVersion 2>/dev/null)" [[ "$v" =~ ^15\. ]] } check_rosetta() { if [[ "$(uname -m)" == "arm64" ]]; then banner "Rosetta Verification" if ! /usr/bin/pgrep oahd >/dev/null 2>&1; then note "Rosetta is not installed. Installing..." /usr/sbin/softwareupdate --install-rosetta --agree-to-license ok "Rosetta installed." else ok "Rosetta is already installed." fi fi } # ----------------------- # Go installation # ----------------------- go_install() { banner "Installing/Upgrading Go" local pkg="/tmp/golang.pkg" case "$(uname -m)" in x86_64) curl -fsSL -o "$pkg" "$GO_URL_AMD64" ;; arm64) curl -fsSL -o "$pkg" "$GO_URL_ARM64" ;; *) err "Unsupported architecture: $(uname -m)"; exit 1 ;; esac # Ensure directory exists (harmless if already present) mkdir -p /usr/local/go || true # Correct target for macOS pkg installer: the root volume /usr/sbin/installer -pkg "$pkg" -target "/" rm -f "$pkg" # Avoid sourcing /etc/profile (breaks under set -u). # Make go available right away for the current process: export PATH="/usr/local/go/bin:$PATH" ok "Go installation completed." } # ----------------------- # External installer (rmmagent-mac.sh) # ----------------------- download_install_script() { banner "Downloading Installation Script" cd "$WORKDIR" curl -fsSL -O "https://raw.githubusercontent.com/mattchis/MacRMM-Script/main/rmmagent-mac.sh" chmod +x "./rmmagent-mac.sh" ok "Installation script downloaded to $WORKDIR/rmmagent-mac.sh" } # ----------------------- # Build / install TRMM agent (from source) # ----------------------- getCSREQBlob() { # Sign the MeshAgent binary to extract a designated requirement, then convert to hex /usr/bin/codesign --detached /opt/tacticalmesh/meshagent.sig -s - /opt/tacticalmesh/meshagent local req_str req_str="$(/usr/bin/codesign -d -r- --detached /opt/tacticalmesh/meshagent.sig /opt/tacticalmesh/meshagent 2>&1 | awk -F ' => ' '/designated/{print $2}')" echo "$req_str" | /usr/bin/csreq -r- -b /tmp/csreq.bin >/dev/null 2>&1 local req_hex req_hex="X'$("/usr/bin/xxd" -p /tmp/csreq.bin | tr -d '\n')'" rm -f /tmp/csreq.bin echo "$req_hex" } agent_compile() { banner "Compiling TacticalRMM Agent" local zip="$WORKDIR/rmmagent.zip" curl -fsSL -o "$zip" "https://github.com/amidaware/rmmagent/archive/refs/heads/master.zip" unzip -q "$zip" -d "$WORKDIR" rm -f "$zip" pushd "$WORKDIR/rmmagent-master" >/dev/null case "$(uname -m)" in x86_64) env CGO_ENABLED=0 GOOS=darwin GOARCH=amd64 go build -ldflags "-s -w" -o "$WORKDIR/temp_rmmagent" ;; arm64) env CGO_ENABLED=0 GOOS=darwin GOARCH=arm64 go build -ldflags "-s -w" -o "$WORKDIR/temp_rmmagent" ;; esac popd >/dev/null ok "Agent compiled to $WORKDIR/temp_rmmagent" } install_agent() { banner "Installing TacticalRMM Agent" cp "$WORKDIR/temp_rmmagent" /usr/local/bin/rmmagent /usr/local/bin/rmmagent -m install \ -meshdir /opt/tacticalmesh \ -api "$RMM_URL" \ -client-id "$RMM_CLIENT_ID" \ -site-id "$RMM_SITE_ID" \ -agent-type "$RMM_AGENT_TYPE" \ -auth "$RMM_AUTH" rm -f "$WORKDIR/temp_rmmagent" /usr/bin/xattr -r -d com.apple.quarantine /opt/tacticalmesh/meshagent || true ok "TacticalRMM agent installed." } update_agent() { banner "Updating TacticalRMM Agent" if [[ ! -f "$WORKDIR/temp_rmmagent" ]]; then err "No built agent found. Run agent_compile first." exit 1 fi /bin/launchctl bootout system /Library/LaunchDaemons/tacticalagent.plist || true cp "$WORKDIR/temp_rmmagent" /opt/tacticalagent/tacticalagent rm -f "$WORKDIR/temp_rmmagent" /bin/launchctl load -w /Library/LaunchDaemons/tacticalagent.plist /usr/bin/xattr -r -d com.apple.quarantine /opt/tacticalmesh/meshagent || true ok "Agent updated." } # ----------------------- # Privacy / TCC (Interactive control) # ----------------------- config_securityandprivacy() { banner "Applying Security & Privacy Exceptions (TCC)" # Paths local MESH="/opt/tacticalmesh/meshagent" local TACT="/opt/tacticalagent/tacticalagent" # DB paths local SYSTEM_TCCDB="/Library/Application Support/com.apple.TCC/TCC.db" local CONSOLE_USER; CONSOLE_USER="$(stat -f%Su /dev/console)" # ---- Clean existing SYSTEM entries for MeshAgent & TacticalAgent (avoid stale rows) sqlite3 "$SYSTEM_TCCDB" "DELETE FROM access WHERE client='$MESH' AND service IN ('kTCCServiceAccessibility','kTCCServiceScreenCapture','kTCCServiceSystemPolicyAllFiles');" || true sqlite3 "$SYSTEM_TCCDB" "DELETE FROM access WHERE client='$TACT' AND service='kTCCServiceSystemPolicyAllFiles';" || true # ---- SYSTEM DB grants (path-only, csreq=NULL) # MeshAgent: Accessibility, Screen Recording, Full Disk Access sqlite3 "$SYSTEM_TCCDB" "REPLACE INTO access VALUES('kTCCServiceAccessibility','$MESH',1,2,4,1,NULL,NULL,0,'UNUSED',NULL,0,NULL,NULL,NULL,NULL,NULL);" || true sqlite3 "$SYSTEM_TCCDB" "REPLACE INTO access VALUES('kTCCServiceScreenCapture','$MESH',1,2,4,1,NULL,NULL,0,NULL,NULL,0,NULL,NULL,NULL,NULL,NULL);" || true sqlite3 "$SYSTEM_TCCDB" "REPLACE INTO access VALUES('kTCCServiceSystemPolicyAllFiles','$MESH',1,2,4,1,NULL,NULL,0,NULL,NULL,0,NULL,NULL,NULL,NULL,NULL);" || true # TacticalAgent: Full Disk Access sqlite3 "$SYSTEM_TCCDB" "REPLACE INTO access VALUES('kTCCServiceSystemPolicyAllFiles','$TACT',1,2,4,1,NULL,NULL,0,NULL,NULL,0,NULL,NULL,NULL,NULL,NULL);" || true # ---- Refresh TCC & relaunch Mesh in user session /usr/bin/killall -HUP tccd 2>/dev/null || true if [[ -n "${CONSOLE_USER:-}" ]]; then sudo -u "$CONSOLE_USER" /usr/bin/killall -HUP tccd 2>/dev/null || true fi if [[ -f "/Library/LaunchAgents/meshagent.plist" ]]; then launchctl bootout gui/$(id -u "$CONSOLE_USER") /Library/LaunchAgents/meshagent.plist 2>/dev/null || true launchctl bootstrap gui/$(id -u "$CONSOLE_USER") /Library/LaunchAgents/meshagent.plist 2>/dev/null || true fi ok "TCC permissions applied (system & user DB) and services refreshed." } # ----------------------- # Uninstall (Agent / Mesh) # ----------------------- uninstall_agent() { banner "Uninstalling TacticalRMM Agent" if [[ -f "/Library/LaunchDaemons/tacticalagent.plist" ]]; then /bin/launchctl bootout system /Library/LaunchDaemons/tacticalagent.plist || true rm -f /Library/LaunchDaemons/tacticalagent.plist fi rm -rf /opt/tacticalagent 2>/dev/null || true rm -f /etc/tacticalagent 2>/dev/null || true sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "DELETE FROM access WHERE client='/opt/tacticalagent/tacticalagent';" || true ok "TacticalRMM Agent uninstalled." } uninstall_mesh() { banner "Uninstalling Mesh Agent" /bin/launchctl bootout system /Library/LaunchDaemons/meshagent.plist 2>/dev/null || true rm -f /Library/LaunchAgents/meshagent-agent.plist 2>/dev/null || true rm -f /Library/LaunchAgents/meshagent.plist 2>/dev/null || true if [[ -x "/opt/tacticalmesh/meshagent" ]]; then /opt/tacticalmesh/meshagent -fulluninstall || true fi rm -rf /opt/tacticalmesh 2>/dev/null || true sqlite3 "/Library/Application Support/com.apple.TCC/TCC.db" "DELETE FROM access WHERE client='/opt/tacticalmesh/meshagent';" || true ok "Mesh Agent uninstalled." } # ----------------------- # Sequoia (macOS 15) fix # ----------------------- sequoia_fix() { banner "Applying Sequoia Fix" rm -f /Library/LaunchAgents/meshagent-agent.plist 2>/dev/null || true rm -f /Library/LaunchDaemons/meshagent.plist 2>/dev/null || true /usr/bin/tee /Library/LaunchAgents/meshagent.plist >/dev/null << 'EOF' Disabled KeepAlive Label meshagent-agent LimitLoadToSessionType Aqua LoginWindow ProgramArguments /opt/tacticalmesh/meshagent --no-embedded=1 --installedByUser=NaN RunAtLoad WorkingDirectory /opt/tacticalmesh EOF chown root:wheel /Library/LaunchAgents/meshagent.plist chmod 644 /Library/LaunchAgents/meshagent.plist chmod 666 /opt/tacticalmesh/meshagent.msh 2>/dev/null || true chmod 666 /opt/tacticalmesh/meshagent.db 2>/dev/null || true ok "Sequoia fix applied." } # ----------------------- # Post-install instructions (User-facing) # ----------------------- show_post_install_instructions() { banner "Installation Complete" cat <<'TXT' ############################################################### # # # INSTALLATION COMPLETE # # # # ✓ Security permissions applied # # ✓ macOS version-specific fixes applied (if needed) # # # # If you need to manually check permissions: # # # # 1. Accessibility: # # -> /opt/tacticalmesh/MeshAgent # # 2. Screen Recording: # # -> /opt/tacticalmesh/MeshAgent # # 3. Full Disk Access: # # -> /opt/tacticalmesh/MeshAgent # # -> /opt/tacticalagent/TacticalAgent # # # # ⚠️ IMPORTANT: Please reboot the Mac to ensure # # all changes take full effect. # # # ############################################################### TXT echo "" echo "TacticalRMM Agent is now installed and configured!" } # ----------------------- # Install modes # ----------------------- auto_install() { banner "Automatic Installation" check_rosetta download_install_script if [[ "$DEBUG" == "true" ]]; then banner "Installation Parameters (Debug Mode)" echo "API_URL : $API_URL" echo "CUSTOMER_ID : $CUSTOMER_ID" echo "GROUP_ID : $GROUP_ID" echo "AGENT_KEY : $AGENT_KEY" echo "AGENT_TYPE : $AGENT_TYPE" else banner "Installation Parameters" fi banner "Starting Installation" (cd "$WORKDIR" && ./rmmagent-mac.sh install \ "$API_URL" \ "$CUSTOMER_ID" \ "$GROUP_ID" \ "$AGENT_KEY" \ "$AGENT_TYPE") banner "Installation Successful" wait_with_progress 30 "Waiting for agent to initialize" config_securityandprivacy show_post_install_instructions } manual_install() { # Args: RMM_URL="$1" RMM_CLIENT_ID="$2" RMM_SITE_ID="$3" RMM_AUTH="$4" RMM_AGENT_TYPE="$5" banner "Manual Installation" echo "API_URL : $RMM_URL" echo "CLIENT_ID : $RMM_CLIENT_ID" echo "SITE_ID : $RMM_SITE_ID" echo "AGENT_TYPE : $RMM_AGENT_TYPE" go_install agent_compile install_agent ok "Manual installation completed." wait_with_progress 30 "Waiting for agent to initialize" config_securityandprivacy show_post_install_instructions } interactive_install() { banner "Interactive Installation" echo "Please enter the following information:" echo "" read -rp "API URL: " RMM_URL [[ -n "${RMM_URL:-}" ]] || { err "API URL cannot be empty!"; exit 1; } read -rp "Client ID: " RMM_CLIENT_ID [[ -n "${RMM_CLIENT_ID:-}" ]] || { err "Client ID cannot be empty!"; exit 1; } read -rp "Site ID: " RMM_SITE_ID [[ -n "${RMM_SITE_ID:-}" ]] || { err "Site ID cannot be empty!"; exit 1; } read -rp "Authentication Key: " RMM_AUTH [[ -n "${RMM_AUTH:-}" ]] || { err "Authentication Key cannot be empty!"; exit 1; } while true; do read -rp "Agent Type (server/workstation): " RMM_AGENT_TYPE if [[ "$RMM_AGENT_TYPE" == "server" || "$RMM_AGENT_TYPE" == "workstation" ]]; then break else echo "Please enter 'server' or 'workstation'" fi done echo "" echo "Installation parameters:" echo "API_URL : $RMM_URL" echo "CLIENT_ID : $RMM_CLIENT_ID" echo "SITE_ID : $RMM_SITE_ID" echo "AGENT_TYPE : $RMM_AGENT_TYPE" echo "" read -rp "Proceed with installation? (y/N): " confirm if [[ "$confirm" != "y" && "$confirm" != "Y" ]]; then echo "Installation cancelled." exit 0 fi go_install agent_compile install_agent ok "Interactive installation completed." wait_with_progress 30 "Waiting for agent to initialize" config_securityandprivacy show_post_install_instructions } # ----------------------- # Help / usage # ----------------------- show_help() { cat <<'HELP' TacticalRMM Agent installation/management script for macOS Usage: script.sh install Manual installation with parameters script.sh auto_install Automatic installation using preconfigured variables script.sh interactive_install Interactive installation (prompts for each parameter) script.sh update Update the agent (build + deploy) script.sh uninstall Complete uninstallation (agent + mesh) script.sh enablepermissions Apply Security & Privacy (TCC) permissions script.sh sequoiafix Apply Sequoia (macOS 15) fix script.sh help Show this help Arguments for manual installation: api_url : RMM API URL client_id : Client ID site_id : Site ID auth_key : Authentication key agent_type : 'server' or 'workstation' More information: github.com/mattchis/rmmagent-macos HELP } validate_args() { local cmd="${1:-}" [[ -n "$cmd" ]] || { err "First argument is missing!"; show_help; exit 1; } case "$cmd" in install|auto_install|interactive_install|update|uninstall|enablepermissions|sequoiafix|help) : ;; *) err "Invalid first argument: $cmd"; show_help; exit 1 ;; esac if [[ "$cmd" == "install" ]]; then if [[ "${2:-}" == "" || "${3:-}" == "" || "${4:-}" == "" || "${5:-}" == "" || "${6:-}" == "" ]]; then err "All arguments are required for manual installation!" echo "Usage: $0 install " exit 1 fi if [[ "${6}" != "server" && "${6}" != "workstation" ]]; then err "Agent type must be 'server' or 'workstation'!" exit 1 fi fi } # ----------------------- # Main # ----------------------- main() { validate_args "${@:-}" [[ "${1:-}" == "help" ]] && { show_help; exit 0; } check_root check_dependencies banner "Script Execution Started" case "$1" in install) manual_install "$2" "$3" "$4" "$5" "$6" ;; auto_install) auto_install ;; interactive_install) interactive_install ;; enablepermissions) local ver ver="$(get_macos_version)" echo "macOS version $ver detected" config_securityandprivacy echo "Security permissions configuration completed." ;; sequoiafix) local ver ver="$(get_macos_version)" echo "macOS version $ver detected" if is_sequoia; then echo "macOS Sequoia confirmed, applying Sequoia fix..." else echo "macOS Sequoia not detected (current: $ver). Applying fix anyway as requested..." fi sequoia_fix echo "Sequoia fix applied. A reboot may be required." ;; update) go_install agent_compile update_agent echo "Agent update completed." wait_with_progress 30 "Waiting for agent to initialize" config_securityandprivacy echo "Update process completed with automatic permission configuration." ;; uninstall) uninstall_agent uninstall_mesh banner "Uninstallation Complete" echo "TacticalRMM/Mesh Agent uninstallation completed." echo "You may need to manually remove orphaned agent connections on TacticalRMM and MeshCentral." ;; esac } main "$@"