#!/bin/sh # Umbra Provider uninstaller for macOS. # # This script stops the per-user host service and removes the Umbra app, CLI # wrapper, and local Umbra data. It deliberately DOES NOT remove the Apple # Device Management profile: macOS requires the owner to review and remove that # profile in System Settings. set -eu YES=0 KEEP_DATA=0 usage() { cat <<'EOF' Usage: sh umbra-uninstall.sh [--yes] [--keep-data] --yes Skip the interactive confirmation after printing the plan. --keep-data Preserve ~/.umbra, including downloaded models and local config. --help Show this help. The Umbra Device Management profile is never removed by this script. Remove it yourself in System Settings > General > Device Management after reviewing it. EOF } while [ "$#" -gt 0 ]; do case "$1" in --yes) YES=1 ;; --keep-data) KEEP_DATA=1 ;; -h|--help) usage; exit 0 ;; *) printf 'Unknown option: %s\n\n' "$1" >&2; usage >&2; exit 2 ;; esac shift done die() { printf 'Umbra uninstall: %s\n' "$*" >&2; exit 1; } say() { printf '%s\n' "$*"; } [ "$(uname -s 2>/dev/null || true)" = "Darwin" ] || die "this uninstaller runs on macOS only" [ -n "${HOME:-}" ] && [ "$HOME" != "/" ] || die "HOME is missing or unsafe" case "$HOME" in /*) ;; *) die "HOME must be an absolute path" ;; esac HOME_PATH="$(CDPATH='' cd -- "$HOME" 2>/dev/null && pwd -P)" || die "HOME does not exist" [ -n "$HOME_PATH" ] && [ "$HOME_PATH" != "/" ] || die "HOME resolves to an unsafe path" APP_DIR="${UMBRA_INSTALL_DIR:-/Applications}" BIN_DIR="${UMBRA_BIN_DIR:-/usr/local/bin}" APP_PATH="$APP_DIR/Umbra Provider.app" CLI_PATH="$BIN_DIR/umbra" HOMEBREW_SHADOW="${UMBRA_HOMEBREW_SHADOW:-/opt/homebrew/bin/umbra}" PLIST_PATH="$HOME_PATH/Library/LaunchAgents/dev.tryumbra.host.plist" DATA_PATH="$HOME_PATH/.umbra" DOMAIN="gui/$(id -u)" HOST_LABEL="dev.tryumbra.host" LAUNCHCTL="${UMBRA_LAUNCHCTL:-/bin/launchctl}" PLISTBUDDY="${UMBRA_PLISTBUDDY:-/usr/libexec/PlistBuddy}" case "$APP_DIR" in /*) ;; *) die "UMBRA_INSTALL_DIR must be an absolute path" ;; esac case "$BIN_DIR" in /*) ;; *) die "UMBRA_BIN_DIR must be an absolute path" ;; esac case "$LAUNCHCTL" in /*) ;; *) die "UMBRA_LAUNCHCTL must be an absolute path" ;; esac case "$PLISTBUDDY" in /*) ;; *) die "UMBRA_PLISTBUDDY must be an absolute path" ;; esac [ "$APP_DIR" != "/" ] || die "refusing unsafe UMBRA_INSTALL_DIR=/" [ "$BIN_DIR" != "/" ] || die "refusing unsafe UMBRA_BIN_DIR=/" [ -x "$LAUNCHCTL" ] || die "launchctl is not executable: $LAUNCHCTL" [ -x "$PLISTBUDDY" ] || die "PlistBuddy is not executable: $PLISTBUDDY" say "" say "Umbra Provider uninstall" say "" say "This will:" say " - stop the per-user dev.tryumbra.host LaunchAgent" say " - remove $PLIST_PATH" say " - remove $APP_PATH" say " - remove the Umbra wrapper at $CLI_PATH (when it matches this app)" if [ "$HOMEBREW_SHADOW" != "$CLI_PATH" ]; then say " - remove the legacy Umbra wrapper at $HOMEBREW_SHADOW (when Umbra owns it)" fi if [ "$KEEP_DATA" -eq 1 ]; then say " - preserve $DATA_PATH (--keep-data)" else say " - remove $DATA_PATH, including downloaded models, local config, and status logs" fi say "" say "This will NOT remove the Umbra Device Management profile." say "You must review and remove that profile yourself in:" say " System Settings > General > Device Management > Umbra > Remove" say "" if [ "$YES" -ne 1 ]; then [ -t 0 ] || die "interactive confirmation required; download this script, inspect it, then run it from Terminal" printf 'Continue? [y/N] ' read -r answer || answer="" case "$answer" in y|Y|yes|YES) ;; *) say "Cancelled. Nothing was removed."; exit 0 ;; esac fi # Stop the managed launchd job directly. Do not execute the installed app while # uninstalling: an incomplete or tampered bundle must never gain execution just # because it occupies the conventional Umbra path. # # A launchd label is shared across the whole GUI domain, not scoped to $HOME. # Only boot out by label when launchd confirms that the loaded job came from the # exact plist this invocation is removing. This prevents alternate-HOME tests # or repair tools from unloading the real user's provider by label. LOADED_PLIST_PATH="$("$LAUNCHCTL" print "$DOMAIN/$HOST_LABEL" 2>/dev/null \ | sed -n 's/^[[:space:]]*path = //p' | sed -n '1p' || true)" if [ "$LOADED_PLIST_PATH" = "$PLIST_PATH" ]; then "$LAUNCHCTL" bootout "$DOMAIN/$HOST_LABEL" >/dev/null 2>&1 || true elif [ -n "$LOADED_PLIST_PATH" ]; then say "Left the loaded $HOST_LABEL job running because it belongs to $LOADED_PLIST_PATH." fi rm -f "$PLIST_PATH" say "Removed the Umbra host LaunchAgent plist." # Only remove the conventional app path when it is actually Umbra's bundle. if [ -e "$APP_PATH" ]; then BUNDLE_ID="$("$PLISTBUDDY" -c 'Print :CFBundleIdentifier' "$APP_PATH/Contents/Info.plist" 2>/dev/null || true)" if [ "$BUNDLE_ID" = "com.umbra.provider" ]; then if rm -rf "$APP_PATH" 2>/dev/null; then :; else say "Administrator approval is required to remove $APP_PATH." sudo rm -rf "$APP_PATH" fi say "Removed $APP_PATH." else say "Left $APP_PATH in place because its bundle identifier was not com.umbra.provider." fi else say "Umbra Provider.app was not present." fi # Official wrappers are tiny /bin/sh launchers, or legacy symlinks directly to # the signed app. Verify ownership before removing either conventional path. is_owned_umbra_wrapper() { candidate="$1" if [ -L "$candidate" ]; then link_target="$(readlink "$candidate" 2>/dev/null || true)" case "$link_target" in *"Umbra Provider.app/Contents/MacOS/umbra") return 0 ;; esac fi [ -f "$candidate" ] || return 1 [ "$(sed -n '1p' "$candidate" 2>/dev/null || true)" = "#!/bin/sh" ] || return 1 grep -Fq 'Umbra Provider.app/Contents/MacOS/umbra' "$candidate" 2>/dev/null } remove_owned_wrapper() { candidate="$1" if [ ! -e "$candidate" ] && [ ! -L "$candidate" ]; then say "Umbra wrapper was not present at $candidate." return 0 fi if ! is_owned_umbra_wrapper "$candidate"; then say "Left $candidate in place because it was not the Umbra installer wrapper." return 0 fi if rm -f "$candidate" 2>/dev/null; then :; else say "Administrator approval is required to remove $candidate." sudo rm -f "$candidate" fi say "Removed $candidate." } remove_owned_wrapper "$CLI_PATH" if [ "$HOMEBREW_SHADOW" != "$CLI_PATH" ]; then remove_owned_wrapper "$HOMEBREW_SHADOW" fi if [ "$KEEP_DATA" -ne 1 ]; then rm -rf "$DATA_PATH" say "Removed $DATA_PATH." fi say "" say "Umbra software removal is complete." say "The Device Management profile remains until you remove it in System Settings."