One-run `larry uninstall` / uninstall-larry.sh that: - stops detached larry.sh REPL + phi-presidio-sidecar + larry-tunnel (pgrep+kill by pattern, never kills itself/parent/uninstall-larry) - SECURELY deletes cleartext PHI (auto-phi.log, lookup.tsv, sessions/*.log.md) via shred -u -z -n 3, with overwrite-then-rm fallback on Windows/MobaXterm where shred is absent, honest per-platform "secure achieved?" reporting, and a find-less bash-glob fallback for session files - strips ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN|LARRY_*|GITEA_TOKEN from shell rc with a timestamped backup (default), or prints them under --keep-rc - removes ~/larry, ~/.local/bin/larry, ~/bin/larry, ~/larry-anywhere (our shims only; foreign `larry` preserved), then self-removes a standalone checkout - prints a FINISH-AT-THE-SOURCE reminder: revoke API key + OAuth grant + PAT, plus a BAA/PHI-disclosure note - hard rm-rf-/ guards (empty/unset/root/$HOME/non-larry LARRY_HOME refused), scoped strictly to the built target list; DRY-RUN default; new --keep-rc and --no-shred flags Tested: full real run, dry-run scope, all rm-rf guards, --keep-data, no-shred(Windows) fallback, idempotency, standalone-checkout self-uninstall. MANIFEST regenerated so the self-update ships it. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
524 lines
29 KiB
Bash
Executable File
524 lines
29 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# uninstall-larry.sh — cleanly and SECURELY remove everything Larry-Anywhere
|
|
# (a.k.a. "Cloverleaf-Larry") put down, in ONE run. Healthcare-grade: PHI-bearing
|
|
# files are securely shredded (where the platform allows) BEFORE the dir is wiped.
|
|
#
|
|
# Reverses the installer EXACTLY: it removes only what Larry-Anywhere created
|
|
# (its install dir / $LARRY_HOME, the `larry` PATH shims, the optional PHI venv,
|
|
# the jq fallback binary, all runtime/PHI artifacts) and NOTHING the installer
|
|
# didn't create. It does NOT touch your Cloverleaf sites or your $HCIROOT. It
|
|
# DOES (by default) strip Larry-related credential exports it finds in your shell
|
|
# rc — with a timestamped backup first — because leaving plaintext secrets behind
|
|
# is the unsafe default for a healthcare deployment (override with --keep-rc).
|
|
#
|
|
# Default is a DRY-RUN preview. You must pass --yes (or confirm at the prompt)
|
|
# to actually delete. Every path removed is printed, per-step success/failure.
|
|
#
|
|
# Usage:
|
|
# uninstall-larry.sh # dry-run: list exactly what WOULD happen
|
|
# uninstall-larry.sh --dry-run # (explicit) same as above
|
|
# uninstall-larry.sh --yes # remove without the interactive prompt
|
|
# uninstall-larry.sh --keep-data # remove program files but KEEP user data
|
|
# # (sessions/, journal/, lessons/,
|
|
# # knowledge/, sanitize/, log/, creds)
|
|
# uninstall-larry.sh --keep-rc # do NOT modify shell rc files; just PRINT
|
|
# # any Larry credential lines for manual
|
|
# # removal (no auto-strip)
|
|
# uninstall-larry.sh --no-shred # skip secure-delete; plain rm (NOT advised
|
|
# # for PHI hosts — kept for slow disks)
|
|
# uninstall-larry.sh --help
|
|
#
|
|
# Also reachable as a subcommand: larry uninstall [flags]
|
|
#
|
|
# Env vars (mirror the installer so it targets the SAME footprint):
|
|
# LARRY_HOME install location (default: $HOME/.larry)
|
|
# LARRY_BIN_DIR primary `larry` shim dir (default: $HOME/bin)
|
|
set -eu
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Paths. NOTE on safety: we intentionally do NOT default LARRY_HOME to a bare
|
|
# expansion that could collapse to "/". If HOME is empty AND LARRY_HOME is unset,
|
|
# the guard below aborts rather than risk rm -rf on a dangerous path.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
LARRY_HOME="${LARRY_HOME:-${HOME:-}/.larry}"
|
|
LARRY_BIN_DIR="${LARRY_BIN_DIR:-${HOME:-}/bin}"
|
|
|
|
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_GREEN=$'\033[32m'
|
|
C_YELLOW=$'\033[33m'; C_RED=$'\033[31m'; C_CYAN=$'\033[36m'; C_DIM=$'\033[2m'
|
|
|
|
say() { printf '%s%suninstall-larry>%s %s\n' "$C_CYAN" "$C_BOLD" "$C_RESET" "$*"; }
|
|
ok() { printf ' %s\xe2\x9c\x93%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
|
|
warn() { printf ' %s!%s %s\n' "$C_YELLOW" "$C_RESET" "$*"; }
|
|
step() { printf '%s\xe2\x80\xa2%s %s\n' "$C_BOLD" "$C_RESET" "$*"; }
|
|
die() { printf '%serror:%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; exit 1; }
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Platform detection (matches install-larry.sh / larry.sh). On Windows-bash
|
|
# (MobaXterm/Cygwin/MSYS/Git-Bash) `shred` is usually ABSENT and there is no
|
|
# Presidio venv — we degrade honestly.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
PLATFORM="unknown"
|
|
case "$(uname -s 2>/dev/null || echo unknown)" in
|
|
Linux*) PLATFORM="linux" ;;
|
|
Darwin*) PLATFORM="darwin" ;;
|
|
CYGWIN*|MINGW*|MSYS*) PLATFORM="windows-cygwin" ;; # MobaXterm lives here
|
|
esac
|
|
[ -n "${MSYSTEM:-}" ] && PLATFORM="windows-cygwin"
|
|
case "${OSTYPE:-}" in cygwin*|msys*) PLATFORM="windows-cygwin" ;; esac
|
|
|
|
HAVE_SHRED=0
|
|
command -v shred >/dev/null 2>&1 && HAVE_SHRED=1
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Args
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
DRY_RUN=1 # default: preview only
|
|
ASSUME_YES=0
|
|
KEEP_DATA=0
|
|
KEEP_RC=0
|
|
NO_SHRED=0
|
|
for arg in "${@:-}"; do
|
|
case "$arg" in
|
|
--yes|-y) DRY_RUN=0; ASSUME_YES=1 ;;
|
|
--dry-run|-n) DRY_RUN=1 ;;
|
|
--keep-data) KEEP_DATA=1 ;;
|
|
--keep-rc) KEEP_RC=1 ;;
|
|
--no-shred) NO_SHRED=1 ;;
|
|
--help|-h) sed -n '2,49p' "$0"; exit 0 ;;
|
|
"") : ;;
|
|
*) die "unknown flag: $arg (try --help)" ;;
|
|
esac
|
|
done
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# HARD SAFETY GUARD — never operate on an empty/unset/root/HOME LARRY_HOME, so a
|
|
# misconfigured env can never turn this into `rm -rf /` or wipe $HOME. This is
|
|
# the single most important guard in the script.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
_norm() { printf '%s' "$1" | sed 's:/*$::'; } # strip trailing slashes
|
|
# If HOME is empty/unset, LARRY_HOME defaulted to "/.larry" — a root-leaf path we
|
|
# must NOT operate on. Require an explicit, absolute, non-root LARRY_HOME.
|
|
if [ -z "${HOME:-}" ] && [ -z "${LARRY_HOME:-}" ]; then
|
|
die "refusing to operate: HOME is unset and LARRY_HOME was not given. Set LARRY_HOME explicitly."
|
|
fi
|
|
_LH_NORM="$(_norm "$LARRY_HOME")"
|
|
case "$_LH_NORM" in
|
|
""|"/"|"/root"|"/home"|"/Users"|"/usr"|"/etc"|"/var"|"/bin"|"/tmp"|"/.larry")
|
|
die "refusing to operate: LARRY_HOME resolves to a dangerous/empty path ('$LARRY_HOME'). Set LARRY_HOME explicitly." ;;
|
|
esac
|
|
# Must be an absolute path with at least two path components (e.g. /home/u/.larry),
|
|
# never a single-component-at-root like /.larry or /larry.
|
|
case "$_LH_NORM" in
|
|
/*/?*) : ;;
|
|
*) die "refusing to operate: LARRY_HOME ('$LARRY_HOME') must be an absolute path below a home/parent dir." ;;
|
|
esac
|
|
if [ -n "${HOME:-}" ] && [ "$_LH_NORM" = "$(_norm "$HOME")" ]; then
|
|
die "refusing to operate: LARRY_HOME equals \$HOME ('$LARRY_HOME'). That cannot be right."
|
|
fi
|
|
# Defense in depth: LARRY_HOME leaf must look like a Larry dir.
|
|
case "$_LH_NORM" in
|
|
*/.larry|*larry*) : ;;
|
|
*) die "refusing to operate: LARRY_HOME ('$LARRY_HOME') doesn't look like a Larry install dir." ;;
|
|
esac
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Where this script physically lives (for the self-uninstall step). When run as
|
|
# $LARRY_HOME/uninstall-larry.sh it's inside the tree we delete; when run from a
|
|
# separate larry-anywhere checkout we also remove that checkout.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
SELF_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd || echo "")"
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 1. STOP EVERYTHING — kill cleanly, tolerate absence. Two layers:
|
|
# (a) pidfiles Larry wrote under $LARRY_HOME (precise — only a PID we own);
|
|
# (b) pgrep+kill by command pattern (catches detached REPLs / keepalive loops
|
|
# with no pidfile). We NEVER kill the uninstaller itself, its parent, or
|
|
# any process whose command contains 'uninstall-larry'.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
stop_bg_proc() { # $1 = label, $2 = pidfile
|
|
local label="$1" pidfile="$2" pid=""
|
|
[ -f "$pidfile" ] || return 0
|
|
pid="$(tr -d '[:space:]' < "$pidfile" 2>/dev/null || echo "")"
|
|
case "$pid" in ''|*[!0-9]*) return 0 ;; esac
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
printf ' %s\xe2\x80\xa2%s would stop %s (PID %s)\n' "$C_YELLOW" "$C_RESET" "$label" "$pid"
|
|
else
|
|
kill "$pid" 2>/dev/null && ok "stopped $label (PID $pid)" \
|
|
|| warn "could not stop $label (PID $pid) — kill it manually"
|
|
fi
|
|
fi
|
|
}
|
|
|
|
SELF_PID="$$"
|
|
SELF_PPID="$(ps -o ppid= -p "$$" 2>/dev/null | tr -d '[:space:]' || echo '')"
|
|
|
|
pkill_pattern() { # $1 = label, $2 = pgrep -f pattern
|
|
local label="$1" pat="$2"
|
|
command -v pgrep >/dev/null 2>&1 || return 0 # no pgrep → nothing safe to do
|
|
local pids="" pid cmd
|
|
pids="$(pgrep -f "$pat" 2>/dev/null || true)"
|
|
for pid in $pids; do
|
|
[ -n "$pid" ] || continue
|
|
case "$pid" in *[!0-9]*) continue ;; esac
|
|
[ "$pid" = "$SELF_PID" ] && continue
|
|
[ -n "$SELF_PPID" ] && [ "$pid" = "$SELF_PPID" ] && continue
|
|
cmd="$(ps -o args= -p "$pid" 2>/dev/null || echo '')"
|
|
case "$cmd" in *uninstall-larry*) continue ;; esac # never kill ourselves
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
printf ' %s\xe2\x80\xa2%s would stop %s (PID %s)\n' "$C_YELLOW" "$C_RESET" "$label" "$pid"
|
|
else
|
|
kill "$pid" 2>/dev/null && ok "stopped $label (PID $pid)" \
|
|
|| warn "could not stop $label (PID $pid) — kill it manually"
|
|
fi
|
|
done
|
|
}
|
|
|
|
say "1) Stopping Larry processes (best-effort, tolerates absence)"
|
|
stop_bg_proc "PHI Presidio sidecar" "$LARRY_HOME/.phi-sidecar.pid"
|
|
stop_bg_proc "reverse SSH tunnel" "$LARRY_HOME/tunnel.pid"
|
|
# Patterns require a path-sep/space before 'larry.sh' so 'uninstall-larry.sh'
|
|
# (this script) is NOT matched; the per-PID uninstall-larry exclusion is backup.
|
|
pkill_pattern "larry.sh REPL" '[/ ]larry\.sh'
|
|
pkill_pattern "phi-presidio-sidecar" 'phi-presidio-sidecar'
|
|
pkill_pattern "larry-tunnel keepalive" 'larry-tunnel'
|
|
echo ""
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 2/3. Build removal lists, separating:
|
|
# - PHI_TARGETS : cleartext-PHI files → SECURE delete first (healthcare)
|
|
# - PROGRAM_TARGETS: program footprint + the rest of $LARRY_HOME
|
|
# - DATA_TARGETS : user data preserved under --keep-data
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
PHI_TARGETS=()
|
|
PROGRAM_TARGETS=()
|
|
DATA_TARGETS=()
|
|
|
|
collect_phi() {
|
|
[ -d "$LARRY_HOME" ] || return 0
|
|
local f
|
|
for f in "$LARRY_HOME/log/auto-phi.log" "$LARRY_HOME/sanitize/lookup.tsv"; do
|
|
[ -f "$f" ] && PHI_TARGETS+=("$f")
|
|
done
|
|
if [ -d "$LARRY_HOME/sessions" ]; then
|
|
if command -v find >/dev/null 2>&1; then
|
|
while IFS= read -r f; do [ -n "$f" ] && PHI_TARGETS+=("$f"); done \
|
|
< <(find "$LARRY_HOME/sessions" -type f -name '*.log.md' 2>/dev/null)
|
|
else
|
|
# find absent (stripped MobaXterm): glob fallback so session PHI is never
|
|
# silently skipped. nullglob so a no-match doesn't add a literal pattern.
|
|
local _ng; _ng="$(shopt -p nullglob 2>/dev/null || true)"
|
|
shopt -s nullglob 2>/dev/null || true
|
|
for f in "$LARRY_HOME"/sessions/*.log.md "$LARRY_HOME"/sessions/**/*.log.md; do
|
|
[ -f "$f" ] && PHI_TARGETS+=("$f")
|
|
done
|
|
eval "$_ng" 2>/dev/null || true
|
|
fi
|
|
fi
|
|
}
|
|
[ "$KEEP_DATA" = "0" ] && collect_phi
|
|
|
|
# Shims / copies. We only remove a `larry` file if it is OUR shim (auto-generated
|
|
# header) OR a symlink/launcher pointing into $LARRY_HOME.
|
|
is_our_shim() { # $1 = path
|
|
local p="$1"
|
|
[ -e "$p" ] || return 1
|
|
grep -q 'Auto-generated by install-larry.sh' "$p" 2>/dev/null && return 0
|
|
grep -q "$LARRY_HOME" "$p" 2>/dev/null && return 0
|
|
if [ -L "$p" ]; then
|
|
case "$(readlink "$p" 2>/dev/null)" in *"$LARRY_HOME"*) return 0 ;; esac
|
|
fi
|
|
return 1
|
|
}
|
|
_seen_shims=""
|
|
for SHIM in "$LARRY_BIN_DIR/larry" \
|
|
"${HOME:-}/larry" "${HOME:-}/.local/bin/larry" "${HOME:-}/bin/larry"; do
|
|
[ -n "$SHIM" ] || continue
|
|
case "$_seen_shims" in *"|$SHIM|"*) continue ;; esac # dedupe (LARRY_BIN_DIR may == $HOME/bin)
|
|
_seen_shims="$_seen_shims|$SHIM|"
|
|
if is_our_shim "$SHIM"; then
|
|
PROGRAM_TARGETS+=("$SHIM")
|
|
elif [ -e "$SHIM" ]; then
|
|
warn "$SHIM exists but is NOT our shim/launcher — leaving it untouched."
|
|
fi
|
|
done
|
|
# A scp'd source folder (install-from-folder path).
|
|
if [ -n "${HOME:-}" ] && [ -d "$HOME/larry-anywhere" ]; then
|
|
PROGRAM_TARGETS+=("$HOME/larry-anywhere")
|
|
fi
|
|
|
|
# The install dir itself.
|
|
if [ -d "$LARRY_HOME" ]; then
|
|
if [ "$KEEP_DATA" = "0" ]; then
|
|
PROGRAM_TARGETS+=("$LARRY_HOME")
|
|
else
|
|
for p in \
|
|
larry.sh larry-tunnel.sh larry-rollback.sh larry-auth.sh uninstall-larry.sh \
|
|
install-larry.sh VERSION MANUAL.md MANIFEST CHANGELOG.md README.md \
|
|
inbound-systems.tsv agents lib bin phi-venv \
|
|
.last-sync-version .origin .oauth-optin-warned \
|
|
.phi-notice-shown .b64-py3-notice-shown \
|
|
.last-curl-rc .last-curl-stderr .last-curl-headers \
|
|
.last-stream-headers .last-stream-curlerr \
|
|
.phi-sidecar.pid tunnel.pid tunnel.url tunnel.log .history; do
|
|
[ -e "$LARRY_HOME/$p" ] && PROGRAM_TARGETS+=("$LARRY_HOME/$p")
|
|
done
|
|
for d in sessions journal lessons knowledge sanitize regression log \
|
|
.api-key .env .oauth.json .ssh-creds .ssh-sockets .ssh-hosts.tsv \
|
|
known_hosts .headers-sync-offset .headers-sync-state; do
|
|
[ -e "$LARRY_HOME/$d" ] && DATA_TARGETS+=("$LARRY_HOME/$d")
|
|
done
|
|
fi
|
|
fi
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 5. Shell-profile credential lines. SAFE, PREDICTABLE behavior:
|
|
# default = back up the rc (timestamped .larry-uninstall.bak) then STRIP the
|
|
# matching lines; --keep-rc = print only, change nothing.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
RC_FILES=()
|
|
for rc in "${HOME:-}/.bashrc" "${HOME:-}/.bash_profile" "${HOME:-}/.profile"; do
|
|
[ -n "$rc" ] && [ -f "$rc" ] && RC_FILES+=("$rc")
|
|
done
|
|
CRED_RE='ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN|LARRY_[A-Z_]*|GITEA_TOKEN'
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Preview
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
if [ "${#PROGRAM_TARGETS[@]}" -eq 0 ] && [ "${#PHI_TARGETS[@]}" -eq 0 ]; then
|
|
say "nothing to remove — no Larry-Anywhere install footprint found."
|
|
say " (looked for LARRY_HOME=$LARRY_HOME and shims under \$HOME)"
|
|
fi
|
|
|
|
say "2/3) Files to remove (PHI files securely shredded first):"
|
|
echo ""
|
|
if [ "${#PHI_TARGETS[@]}" -gt 0 ]; then
|
|
printf '%sCleartext-PHI files (SECURE delete):%s\n' "$C_BOLD" "$C_RESET"
|
|
for t in "${PHI_TARGETS[@]}"; do printf ' %sshred%s %s\n' "$C_RED" "$C_RESET" "$t"; done
|
|
echo ""
|
|
fi
|
|
printf '%sProgram/footprint (removed):%s\n' "$C_BOLD" "$C_RESET"
|
|
if [ "${#PROGRAM_TARGETS[@]}" -eq 0 ]; then printf ' %s(none)%s\n' "$C_DIM" "$C_RESET"; fi
|
|
for t in "${PROGRAM_TARGETS[@]}"; do
|
|
if [ -d "$t" ]; then printf ' %s(dir) %s%s\n' "$C_DIM" "$t" "$C_RESET"
|
|
else printf ' %s\n' "$t"; fi
|
|
done
|
|
if [ "$KEEP_DATA" = "1" ]; then
|
|
echo ""
|
|
printf '%sUser data PRESERVED (--keep-data):%s\n' "$C_BOLD" "$C_RESET"
|
|
if [ "${#DATA_TARGETS[@]}" -eq 0 ]; then printf ' %s(none found)%s\n' "$C_DIM" "$C_RESET"
|
|
else for d in "${DATA_TARGETS[@]}"; do printf ' %skept%s %s\n' "$C_GREEN" "$C_RESET" "$d"; done; fi
|
|
fi
|
|
|
|
# Secure-delete capability honesty (esp. Windows/MobaXterm).
|
|
echo ""
|
|
if [ "$NO_SHRED" = "1" ]; then
|
|
warn "secure-delete DISABLED (--no-shred): PHI files will be removed with plain rm."
|
|
elif [ "$HAVE_SHRED" = "1" ]; then
|
|
say "secure-delete: 'shred -u' available -> PHI files will be cryptographically shredded."
|
|
else
|
|
say "secure-delete: 'shred' NOT found on this platform ($PLATFORM) -> falling back to"
|
|
say " best-effort overwrite-then-remove. NOTE: on Windows/MobaXterm and on copy-on-write"
|
|
say " or SSD/flash filesystems, overwrite-in-place does NOT guarantee the original blocks"
|
|
say " are gone. This will be reported per-file as 'overwrite (best-effort)'."
|
|
fi
|
|
|
|
# Shell rc preview
|
|
echo ""
|
|
say "5) Shell-profile credential lines:"
|
|
RC_HITS=0
|
|
for rc in "${RC_FILES[@]:-}"; do
|
|
[ -n "$rc" ] || continue
|
|
hits="$(grep -nE "$CRED_RE" "$rc" 2>/dev/null || true)"
|
|
if [ -n "$hits" ]; then
|
|
RC_HITS=1
|
|
printf ' %s%s%s:\n' "$C_BOLD" "$rc" "$C_RESET"
|
|
printf '%s\n' "$hits" | sed 's/^/ /'
|
|
fi
|
|
done
|
|
if [ "$RC_HITS" = "0" ]; then
|
|
printf ' %s(no Larry credential exports found in .bashrc/.bash_profile/.profile)%s\n' "$C_DIM" "$C_RESET"
|
|
elif [ "$KEEP_RC" = "1" ]; then
|
|
say " --keep-rc: the lines above will be PRINTED ONLY — remove them by hand."
|
|
else
|
|
say " default: a timestamped backup will be written, then these lines auto-stripped."
|
|
fi
|
|
echo ""
|
|
|
|
if [ "$DRY_RUN" = "1" ]; then
|
|
say "${C_BOLD}DRY-RUN${C_RESET} — nothing was changed. Re-run with ${C_CYAN}--yes${C_RESET} to execute the above."
|
|
exit 0
|
|
fi
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# Confirm
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
if [ "$ASSUME_YES" != "1" ]; then
|
|
printf '%sProceed with uninstall? This cannot be undone. [y/N]: %s' "$C_YELLOW" "$C_RESET"
|
|
ans=""
|
|
if [ -t 0 ]; then read -r ans </dev/tty || ans=""; else read -r ans || ans=""; fi
|
|
case "$ans" in y|Y|yes|YES) : ;; *) say "aborted — nothing changed."; exit 0 ;; esac
|
|
fi
|
|
echo ""
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 3. SECURE-DELETE the cleartext-PHI files FIRST, with honest per-file reporting.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
secure_delete() { # $1 = file. echoes the method actually achieved.
|
|
local f="$1"
|
|
[ -f "$f" ] || { echo "absent"; return 0; }
|
|
if [ "$NO_SHRED" = "1" ]; then rm -f "$f" 2>/dev/null && echo "plain-rm" || echo "FAILED"; return 0; fi
|
|
if [ "$HAVE_SHRED" = "1" ]; then
|
|
if shred -u -z -n 3 "$f" 2>/dev/null; then echo "shred"; return 0; fi
|
|
if shred -u "$f" 2>/dev/null; then echo "shred"; return 0; fi
|
|
fi
|
|
# Fallback: overwrite-in-place then remove. Best-effort on Windows/CoW/SSD.
|
|
local sz
|
|
sz="$(wc -c < "$f" 2>/dev/null || echo 0)"
|
|
if [ "${sz:-0}" -gt 0 ] 2>/dev/null; then
|
|
if command -v dd >/dev/null 2>&1; then
|
|
dd if=/dev/zero of="$f" bs=1 count="$sz" conv=notrunc 2>/dev/null || true
|
|
[ -r /dev/urandom ] && dd if=/dev/urandom of="$f" bs=1 count="$sz" conv=notrunc 2>/dev/null || true
|
|
else
|
|
: > "$f" 2>/dev/null || true # truncate at least
|
|
fi
|
|
fi
|
|
rm -f "$f" 2>/dev/null && echo "overwrite" || echo "FAILED"
|
|
}
|
|
|
|
if [ "${#PHI_TARGETS[@]}" -gt 0 ]; then
|
|
step "Securely deleting cleartext-PHI files"
|
|
phi_shredded=0; phi_besteffort=0; phi_failed=0
|
|
for f in "${PHI_TARGETS[@]}"; do
|
|
method="$(secure_delete "$f")"
|
|
case "$method" in
|
|
shred) ok "shredded (secure): $f"; phi_shredded=$((phi_shredded+1)) ;;
|
|
overwrite) warn "overwrite-then-rm (best-effort, NOT guaranteed on this FS): $f"; phi_besteffort=$((phi_besteffort+1)) ;;
|
|
plain-rm) warn "plain rm (--no-shred): $f"; phi_besteffort=$((phi_besteffort+1)) ;;
|
|
absent) : ;;
|
|
*) warn "FAILED to remove PHI file (remove by hand!): $f"; phi_failed=$((phi_failed+1)) ;;
|
|
esac
|
|
done
|
|
echo ""
|
|
if [ "$HAVE_SHRED" = "1" ] && [ "$NO_SHRED" = "0" ]; then
|
|
say "PHI secure-delete: $phi_shredded shredded, $phi_besteffort best-effort, $phi_failed failed."
|
|
else
|
|
say "PHI delete on $PLATFORM: secure shred UNAVAILABLE — $phi_besteffort file(s) removed best-effort,"
|
|
say " $phi_failed failed. Treat the underlying disk as potentially still containing PHI remnants;"
|
|
say " follow your data-handling policy for media sanitization if required."
|
|
fi
|
|
echo ""
|
|
fi
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 2/4. Remove the rest of the footprint (program files, shims, copies, the dir).
|
|
# Scoped strictly to the built list — no glob expansion, no parent touch.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
removed=0; failed=0
|
|
if [ "${#PROGRAM_TARGETS[@]}" -gt 0 ]; then
|
|
step "Removing program files, shims and the install dir"
|
|
for t in "${PROGRAM_TARGETS[@]}"; do
|
|
if rm -rf "$t" 2>/dev/null; then ok "removed $t"; removed=$((removed+1))
|
|
else warn "could not remove $t (check permissions; remove by hand)"; failed=$((failed+1)); fi
|
|
done
|
|
say "removed $removed item(s); $failed failed."
|
|
echo ""
|
|
fi
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 5. Strip Larry credential lines from shell rc (default), with backup. Skipped
|
|
# under --keep-rc (already printed for manual removal in the preview).
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
if [ "$KEEP_RC" = "0" ] && [ "${#RC_FILES[@]}" -gt 0 ]; then
|
|
step "Stripping Larry credential exports from shell rc (backup first)"
|
|
ts="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo bak)"
|
|
for rc in "${RC_FILES[@]}"; do
|
|
if grep -qE "$CRED_RE" "$rc" 2>/dev/null; then
|
|
cp -p "$rc" "$rc.larry-uninstall.$ts.bak" 2>/dev/null \
|
|
&& ok "backed up $rc -> $rc.larry-uninstall.$ts.bak" \
|
|
|| warn "could not back up $rc — leaving it UNCHANGED for safety"
|
|
if [ -f "$rc.larry-uninstall.$ts.bak" ]; then
|
|
if grep -vE "$CRED_RE" "$rc" > "$rc.larry-tmp" 2>/dev/null && mv "$rc.larry-tmp" "$rc" 2>/dev/null; then
|
|
ok "stripped Larry credential lines from $rc"
|
|
else
|
|
rm -f "$rc.larry-tmp" 2>/dev/null || true
|
|
warn "could not rewrite $rc — remove the credential lines by hand"
|
|
fi
|
|
fi
|
|
fi
|
|
done
|
|
echo ""
|
|
fi
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 6. SELF-UNINSTALL LAST. If this script ran from a separate larry-anywhere
|
|
# checkout (not inside $LARRY_HOME, which is already gone), remove that too —
|
|
# but ONLY if we actually performed an uninstall this run AND the dir holds a
|
|
# full bundle. Then remove this script file itself.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
step "Self-uninstall (removing the larry-anywhere install dir / this script)"
|
|
DID_WORK=0
|
|
[ "$removed" -gt 0 ] && DID_WORK=1
|
|
[ "${#PHI_TARGETS[@]}" -gt 0 ] && DID_WORK=1
|
|
if [ -n "$SELF_DIR" ] && [ -d "$SELF_DIR" ]; then
|
|
case "$(_norm "$SELF_DIR")" in
|
|
"$_LH_NORM") : ;; # was inside LARRY_HOME, already removed above
|
|
*/larry-anywhere|*cloverleaf-larry*|*/.larry)
|
|
if [ "$DID_WORK" = "1" ] && { [ -f "$SELF_DIR/larry.sh" ] || [ -f "$SELF_DIR/install-larry.sh" ]; }; then
|
|
rm -rf "$SELF_DIR" 2>/dev/null && ok "removed self checkout: $SELF_DIR" \
|
|
|| warn "could not remove $SELF_DIR — remove by hand"
|
|
else
|
|
warn "left $SELF_DIR in place (nothing uninstalled this run, or not a full bundle)."
|
|
fi ;;
|
|
*) warn "running from $SELF_DIR — not a recognized Larry dir, NOT self-removing it." ;;
|
|
esac
|
|
fi
|
|
# Remove this script file itself if it still exists.
|
|
if [ -f "$0" ]; then rm -f "$0" 2>/dev/null && ok "removed $0" || true; fi
|
|
echo ""
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# FINAL "confirm gone" check
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
step "Confirming removal"
|
|
gone=1
|
|
if [ -e "$LARRY_HOME" ] && [ "$KEEP_DATA" = "0" ]; then warn "$LARRY_HOME still exists"; gone=0; else ok "$LARRY_HOME removed"; fi
|
|
if command -v larry >/dev/null 2>&1; then
|
|
warn "a 'larry' command is still on PATH ($(command -v larry)) — if it isn't ours, that's fine"
|
|
else
|
|
ok "no 'larry' command on PATH"
|
|
fi
|
|
[ "$gone" = "1" ] && say "${C_GREEN}uninstall complete — local footprint gone.${C_RESET}" \
|
|
|| say "${C_YELLOW}uninstall finished with leftovers — see warnings above.${C_RESET}"
|
|
echo ""
|
|
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
# 7. POST-UNINSTALL REMINDER — non-negotiable for a healthcare deployment.
|
|
# Local removal does NOT invalidate credentials that may have egressed.
|
|
# ─────────────────────────────────────────────────────────────────────────────
|
|
printf '%s%s================ IMPORTANT: FINISH AT THE SOURCE ================%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET"
|
|
cat <<'EOF'
|
|
Removing the files here does NOT revoke the credentials. Any copy that already
|
|
egressed (e.g. exported in another shell, scp'd, or cached elsewhere) remains
|
|
live until you revoke it AT ANTHROPIC. Do BOTH now:
|
|
|
|
1. Anthropic Console -> Settings -> API Keys:
|
|
DELETE/REVOKE the API key minted for this machine (do not just disable).
|
|
https://console.anthropic.com
|
|
|
|
2. Anthropic Console -> account security / Connected apps / sessions:
|
|
REVOKE the Claude-Code OAuth authorization grant and "sign out everywhere"
|
|
(a .oauth.json refresh token can still mint access tokens until revoked).
|
|
|
|
3. Revoke any Gitea PAT (LARRY_GITEA_TOKEN / GITEA_TOKEN) that was set, and
|
|
rotate any SSH credential this box held to your Mac / hop host.
|
|
|
|
HEALTHCARE / PHI / BAA reminder:
|
|
This agent ran on an HL7 integration engine and had a path to PHI. It may have
|
|
transmitted PHI to a third party (Anthropic) and stored cleartext PHI locally
|
|
(now deleted). Review the engagement BAA and, absent clear evidence no PHI
|
|
ever flowed, treat client notification / PHI-disclosure as a decision you must
|
|
make deliberately — do not assume local deletion closes the obligation.
|
|
EOF
|
|
printf '%s%s=================================================================%s\n' "$C_YELLOW" "$C_BOLD" "$C_RESET"
|