diff --git a/CHANGELOG.md b/CHANGELOG.md index f89f9e9..472d5cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,54 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here. Versioning is loose-semver; bumps trigger the in-process self-update on every running client via `LARRY_BASE_URL` + `MANIFEST`. +## v0.8.34 — 2026-05-31 + +**Hardened `uninstall-larry.sh` into a first-class, healthcare-grade decommission +command — "update larry, then run one command."** + +The v0.8.33 uninstaller removed the install footprint but did not meet the +operational decommission requirements for a PHI-handling deployment left on a +client box. This release makes `larry uninstall` (and the shipped +`uninstall-larry.sh` it delegates to) self-contained and idempotent: one run +stops everything, securely destroys cleartext PHI, scrubs creds, self-removes, +and tells you what you still must do at the source. The `larry uninstall` +early-dispatch in `larry.sh` is unchanged — it already delegates here. + +### New / changed behavior +- **Stop everything (tolerant):** in addition to the precise pidfile kills (PHI + Presidio sidecar, reverse-SSH tunnel), it now `pgrep`+kills detached + `larry.sh` REPLs, `phi-presidio-sidecar`, and `larry-tunnel` keepalives by + command pattern. It NEVER kills itself, its parent, or any `uninstall-larry` + process (patterns + per-PID exclusion). +- **PHI hygiene (healthcare):** `log/auto-phi.log`, `sanitize/lookup.tsv`, and + `sessions/*.log.md` are SECURELY deleted FIRST — `shred -u -z -n 3` where + available; overwrite-then-rm fallback (zero + urandom passes) where `shred` is + absent (Windows/MobaXterm); truncate-then-rm if even `dd` is missing. It + prints HONESTLY per-file whether a real secure-delete was achieved, and warns + that overwrite-in-place is not guaranteed on SSD/CoW/Windows filesystems. + `find`-less hosts use a bash-glob fallback so session PHI is never skipped. +- **Shell-profile creds:** greps `.bashrc`/`.bash_profile`/`.profile` for + `ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN|LARRY_*|GITEA_TOKEN` and, by + default, backs up the rc (timestamped `.larry-uninstall..bak`) then strips + only those lines. `--keep-rc` prints them for manual removal instead. +- **More shim/copy locations:** removes `~/larry`, `~/.local/bin/larry`, + `~/bin/larry`, `$LARRY_BIN_DIR/larry`, and a scp'd `~/larry-anywhere` — but + only OUR shims (auto-generated header or a launcher/symlink into `$LARRY_HOME`); + a foreign `larry` is left untouched. +- **Self-uninstall last:** when run from a standalone larry-anywhere checkout + (not from inside `$LARRY_HOME`), removes that checkout and the script itself — + only if real work was done this run and the dir is a full bundle. +- **Post-uninstall reminder:** prints the explicit "FINISH AT THE SOURCE" block — + REVOKE the Anthropic API key AND the Claude-Code OAuth grant (local removal + does not invalidate egressed copies), revoke any Gitea PAT / rotate SSH creds, + plus a BAA/PHI-disclosure reminder for healthcare deployments. +- **Safety guards:** refuses to operate if `LARRY_HOME` is empty/unset, `/`, + `$HOME`, a single-component-at-root path (`/.larry`), or anything that doesn't + look like a Larry dir — so a misconfigured env can never `rm -rf /`. Removal is + scoped strictly to the built target list (no glob expansion, no parent touch). +- New flags: `--keep-rc`, `--no-shred` (alongside the existing `--yes`, + `--dry-run`, `--keep-data`). DRY-RUN remains the default. + ## v0.8.33 — 2026-05-29 **Two operator-requested features: a real uninstaller, and a deterministic-only diff --git a/MANIFEST b/MANIFEST index 6cb4ef9..dcb26b3 100644 --- a/MANIFEST +++ b/MANIFEST @@ -28,12 +28,12 @@ larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 install-larry.sh 0779dd74c0cecef174edbe7782da275be9bf6d2ec7e6ae88c1de46c666adc8b2 -uninstall-larry.sh 5d0e2fe1366c818dc207f70c85e38279218b58f5eae10a8a6b0a277a02f2e4d0 +uninstall-larry.sh c53ad2d8354c7adeb243b541f027f3f481e4a8661eecfd7af14d7ca53cfcaad9 # Metadata -VERSION d9d12c30d3607edd1bfd2ef0feb0738f917e113a4bc1570d5dbb2ea8426531a8 +VERSION 437e52707aa48e9ee29b4469dc0790ad646390c08644c415a5171052487fd280 MANUAL.md 5ff54d6d5fae826f8b3da1eb3be6476076bb15f9b1417a4de285e59ea37e1b1f -CHANGELOG.md 67ed1238990e96d481ffa804cbfcbf219958438d6285e6efe864a33b8df97a32 +CHANGELOG.md 25b82ebf360bb345bb1d7008386532773e9bf55d67b331a7d572d63ff4ab87ae # Agent personas (system-prompt overlays) agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1 diff --git a/VERSION b/VERSION index f901547..b326a53 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.8.33 +0.8.34 diff --git a/uninstall-larry.sh b/uninstall-larry.sh index fdbc379..221253e 100755 --- a/uninstall-larry.sh +++ b/uninstall-larry.sh @@ -1,109 +1,257 @@ #!/usr/bin/env bash -# uninstall-larry.sh — cleanly remove everything install-larry.sh put down. +# 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, the `larry` PATH shim, the optional PHI venv, the jq -# fallback binary, and all runtime artifacts under $LARRY_HOME) and NOTHING the -# installer didn't create. It does NOT touch your shell rc, your Cloverleaf -# sites, your $HCIROOT, or any file outside the install footprint. +# (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. +# to actually delete. Every path removed is printed, per-step success/failure. # # Usage: -# uninstall-larry.sh # dry-run: list exactly what WOULD be removed +# 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 [--dry-run|--yes|--keep-data] +# 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 where the `larry` shim was symlinked (default: $HOME/bin) +# LARRY_BIN_DIR primary `larry` shim dir (default: $HOME/bin) set -eu -LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" -LARRY_BIN_DIR="${LARRY_BIN_DIR:-$HOME/bin}" +# ───────────────────────────────────────────────────────────────────────────── +# 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✓%s %s\n' "$C_GREEN" "$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 ;; - --help|-h) sed -n '2,38p' "$0"; exit 0 ;; + --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 # ───────────────────────────────────────────────────────────────────────────── -# Stop background processes Larry may have started, BEFORE deleting their files. -# Both PID files live under $LARRY_HOME. Best-effort: a stale/dead PID is fine. -# We only kill a PID we can read from Larry's own pidfile — never a guessed one. +# 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. # ───────────────────────────────────────────────────────────────────────────── -stop_bg_proc() { - # $1 = human label, $2 = pidfile path +_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 ;; # empty or non-numeric → nothing safe to do - esac + case "$pid" in ''|*[!0-9]*) return 0 ;; esac if kill -0 "$pid" 2>/dev/null; then if [ "$DRY_RUN" = "1" ]; then - printf ' %s•%s would stop %s (PID %s)\n' "$C_YELLOW" "$C_RESET" "$label" "$pid" + 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" + kill "$pid" 2>/dev/null && ok "stopped $label (PID $pid)" \ + || warn "could not stop $label (PID $pid) — kill it manually" fi fi } -# ───────────────────────────────────────────────────────────────────────────── -# Build the removal list. We separate PROGRAM artifacts (always removed) from -# USER DATA (removed unless --keep-data). Everything here was created by the -# installer or by Larry at runtime — we never list anything else. -# ───────────────────────────────────────────────────────────────────────────── -PROGRAM_TARGETS=() # files/dirs that ARE the install footprint -DATA_TARGETS=() # user-generated data under $LARRY_HOME +SELF_PID="$$" +SELF_PPID="$(ps -o ppid= -p "$$" 2>/dev/null | tr -d '[:space:]' || echo '')" -# The shim the installer drops onto PATH. We only remove it if it is OUR shim -# (auto-generated header), so we never delete an unrelated `larry` someone else -# put there. -SHIM="$LARRY_BIN_DIR/larry" -if [ -f "$SHIM" ] && grep -q 'Auto-generated by install-larry.sh' "$SHIM" 2>/dev/null; then - PROGRAM_TARGETS+=("$SHIM") -elif [ -e "$SHIM" ]; then - warn "$SHIM exists but is NOT our auto-generated shim — leaving it untouched." +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 entire install dir is ours. Rather than enumerate every dotfile, we list -# $LARRY_HOME itself for the full uninstall. With --keep-data we instead remove -# the program files inside it and preserve the user-data subtrees. +# The install dir itself. if [ -d "$LARRY_HOME" ]; then if [ "$KEEP_DATA" = "0" ]; then PROGRAM_TARGETS+=("$LARRY_HOME") else - # Program files (shipped bundle + caches + state markers + the jq fallback - # + the optional PHI venv). Anything not in this list under $LARRY_HOME is - # treated as user data and preserved. 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 \ @@ -115,7 +263,6 @@ if [ -d "$LARRY_HOME" ]; then .phi-sidecar.pid tunnel.pid tunnel.url tunnel.log .history; do [ -e "$LARRY_HOME/$p" ] && PROGRAM_TARGETS+=("$LARRY_HOME/$p") done - # User data preserved under --keep-data (listed for transparency). 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 @@ -124,18 +271,34 @@ if [ -d "$LARRY_HOME" ]; then fi fi -if [ "${#PROGRAM_TARGETS[@]}" -eq 0 ]; then - say "nothing to remove — no Larry-Anywhere install footprint found." - say " (looked for LARRY_HOME=$LARRY_HOME and shim $SHIM)" - exit 0 -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 # ───────────────────────────────────────────────────────────────────────────── -say "Larry-Anywhere uninstall — install footprint to remove:" +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 "" -printf '%sProgram files (always removed):%s\n' "$C_BOLD" "$C_RESET" +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 @@ -143,64 +306,218 @@ 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 + 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 -echo "" -say "NOT touched: your shell rc (any PATH export you added stays — remove it by hand)," -say " your Cloverleaf sites / \$HCIROOT, and anything outside the list above." -echo "" -# Stop background procs (preview prints "would stop …"; real run kills them). -stop_bg_proc "PHI Presidio sidecar" "$LARRY_HOME/.phi-sidecar.pid" -stop_bg_proc "reverse SSH tunnel" "$LARRY_HOME/tunnel.pid" +# 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 removed. Re-run with ${C_CYAN}--yes${C_RESET} to delete the above." + 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 (skipped with --yes... but --yes already set ASSUME_YES; this is the -# belt-and-suspenders path if someone sets DRY_RUN=0 by other means). +# Confirm # ───────────────────────────────────────────────────────────────────────────── if [ "$ASSUME_YES" != "1" ]; then - printf '%sProceed with removal? This cannot be undone. [y/N]: %s' "$C_YELLOW" "$C_RESET" + printf '%sProceed with uninstall? This cannot be undone. [y/N]: %s' "$C_YELLOW" "$C_RESET" ans="" if [ -t 0 ]; then read -r ans /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. Print every path as it goes. rm -rf is scoped strictly to the list we -# built above — we never expand globs or touch a parent dir. -# ───────────────────────────────────────────────────────────────────────────── -removed=0 -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)" - fi -done - +# 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 "" -say "removed $removed item(s)." -if [ "$KEEP_DATA" = "1" ] && [ "${#DATA_TARGETS[@]}" -gt 0 ]; then - say "your data is preserved under $LARRY_HOME — delete it manually if you want a full wipe." + +# ───────────────────────────────────────────────────────────────────────────── +# 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 -# Reminder: the installer never EDITS your shell rc; it only WARNS you to add a -# PATH export. If you added one, it's still there — this is the one manual step. -case ":$PATH:" in - *":$LARRY_BIN_DIR:"*) - say "reminder: you may have added 'export PATH=\"$LARRY_BIN_DIR:\$PATH\"' to your shell rc. Remove that line by hand if $LARRY_BIN_DIR is now empty." ;; -esac -say "uninstall complete." +[ "$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"