v0.8.34: harden uninstall-larry.sh into a first-class PHI-grade decommission
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>
This commit is contained in:
parent
7606a535c9
commit
6b45543652
48
CHANGELOG.md
48
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.<ts>.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
|
||||
|
||||
6
MANIFEST
6
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
|
||||
|
||||
@ -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/tty || ans=""; else read -r ans || ans=""; fi
|
||||
case "$ans" in
|
||||
y|Y|yes|YES) : ;;
|
||||
*) say "aborted — nothing removed."; exit 0 ;;
|
||||
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. 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"
|
||||
|
||||
Loading…
Reference in New Issue
Block a user