cloverleaf-larry/install-larry.sh
Clover 400398ca7d v0.9.6: help canonical + prefix-free short commands (nc-find→nfind guard) + HCISITEDIR auto-init
Hands-on ergonomics for Bryan's Gundersen testing. All three changes are
backward-compatible — every old name still works.

1. `help` is now the canonical reference command (live, never-drifts table
   from bin/ + each tool's help block). `cheat` kept as a thin alias.

2. Prefix-free short commands: nc-table→table, nc-parse→parse, nc-msgs→msgs,
   nc-status→status, nc-engine→engine, nc-xlate→xlate, nc-inbound→inbound, plus
   the write tools (create-thread, set-field, insert-protocol, make-jump,
   provision-jumps, tclgen, document, revisions, diff-interface, smat-diff,
   regression). COLLISION GUARD: nc-find→`nfind` (NOT `find` — would shadow the
   system find on PATH); nc-paths keeps `paths`. Every nc-* name retained as a
   backward-compat alias. Tab-completion + the help/cheat table updated.

3. HCISITEDIR auto-init in the shared preflight (bin/_nc_common.sh): HCIROOT +
   HCISITE still required, but $HCIROOT/$HCISITE is created if missing rather
   than erroring. Conservative + idempotent; respects an operator-set HCISITEDIR.

VERSION→0.9.6, MANIFEST regenerated (--check clean), bash -n clean.

Co-Authored-By: Clover (Claude Opus 4.8) <noreply@anthropic.com>
2026-06-08 20:21:32 -07:00

546 lines
30 KiB
Bash
Executable File

#!/usr/bin/env bash
# install-larry.sh — bootstrap Larry-Anywhere on a fresh remote shell.
# No root, no package install, no sudo. Writes only into $LARRY_HOME.
#
# Usage:
# curl -fsSL <BASE_URL>/install-larry.sh | bash
#
# Or with explicit base URL:
# LARRY_BASE_URL=https://example.com/larry-anywhere bash install-larry.sh
#
# Env vars:
# LARRY_HOME install location (default: $HOME/.larry)
# LARRY_BASE_URL where to fetch files from (no trailing slash)
# LARRY_BIN_DIR where to symlink the `larry` command (default: $HOME/bin)
# LARRY_GITEA_TOKEN optional Gitea PAT (read scope) for authenticated fetch
# against a PRIVATE repo. Alias: GITEA_TOKEN. Never logged.
set -eu
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
# Canonical hosting (v0.7.4): self-hosted Gitea at git.bjnoela.com is the
# single source. The v0.7.2 GitHub fallback was removed after the GitHub
# mirror was made private (anonymous raw fetches now 401/403, so the
# fallback was functionally broken). Override LARRY_BASE_URL via env if you
# fork or mirror elsewhere.
#
# IMPORTANT: the Gitea repo must be PUBLIC for unauthenticated raw-URL reads
# to succeed. If the install fetch fails, set LARRY_BASE_URL to a reachable
# mirror or check repo visibility on git.bjnoela.com.
LARRY_BASE_URL="${LARRY_BASE_URL:-https://git.bjnoela.com/bryan/cloverleaf-larry/raw/branch/main}"
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'
say() { printf '%s%sinstall-larry>%s %s\n' "$C_CYAN" "$C_BOLD" "$C_RESET" "$*"; }
ok() { printf ' %s✓%s %s\n' "$C_GREEN" "$C_RESET" "$*"; }
warn() { printf ' %s!%s %s\n' "$C_YELLOW" "$C_RESET" "$*"; }
die() { printf '%serror:%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; exit 1; }
# >>> fetch-safe inline (keep in sync with lib/fetch-safe.sh) >>>
# install-larry.sh is the curl|bash bootstrap — it runs BEFORE any lib/ file
# exists on disk, so it cannot source lib/fetch-safe.sh. We inline a byte-
# identical copy of the validators. Root cause + design: see
# Deliverables/2026-05-27-cloverleaf-larry-stuck-update-and-tab-bug.md and
# lib/fetch-safe.sh's header. The trap: Gitea answers an unauthenticated raw
# read with HTTP 200 + the HTML Sign-In page; `curl -fsSL` calls that success
# and the installer parses HTML as file content. We detect + fail loud.
_fs_curl_auth_args() {
local _tok="${LARRY_GITEA_TOKEN:-${GITEA_TOKEN:-}}"
_tok="${_tok//$'\r'/}"
if [ -n "$_tok" ]; then
printf '%s\n' '-H'
printf '%s\n' "Authorization: token $_tok"
fi
}
_fs_html_trap_error() {
printf 'error: %s returned an HTML sign-in page, not file content. The Gitea repo is private or the instance requires sign-in. Either (a) make the repo public + set REQUIRE_SIGNIN_VIEW=false, or (b) set LARRY_GITEA_TOKEN=<PAT> for authenticated fetch.\n' \
"$1" >&2
}
_fs_snippet() {
local f="$1" fb="$2" s
s="$(head -c 60 "$f" 2>/dev/null | tr -d '\r\n' )"
[ -z "$s" ] && s="$fb"
printf '"%s..."' "$s"
}
# fetch_validate URL DEST KIND [MAX_TIME] — see lib/fetch-safe.sh for the full
# contract. KIND in {version,manifest,script,sh,text}.
fetch_validate() {
local url="$1" dest="$2" kind="${3:-text}" mt="${4:-15}"
local tmp hdr code ctype first line1
tmp="$(mktemp 2>/dev/null || echo "${dest}.fs.$$")"
hdr="$(mktemp 2>/dev/null || echo "${dest}.fsh.$$")"
local _args=( -sSL --max-time "$mt" -o "$tmp" -D "$hdr" -w '%{http_code}' )
local _auth_line
while IFS= read -r _auth_line; do
[ -n "$_auth_line" ] && _args+=( "$_auth_line" )
done < <(_fs_curl_auth_args)
code="$(curl "${_args[@]}" "$url" 2>/dev/null)"
local rc=$?
code="${code//$'\r'/}"
if [ "$rc" -ne 0 ] || [ ! -s "$tmp" ]; then
rm -f "$tmp" "$hdr"
printf 'error: %s — fetch failed (curl rc=%s). Origin unreachable or timed out.\n' "$url" "$rc" >&2
return 1
fi
ctype="$(grep -i '^content-type:' "$hdr" 2>/dev/null | tail -1 | tr -d '\r' | tr 'A-Z' 'a-z')"
first="$(head -c 4096 "$tmp" 2>/dev/null | tr -d '\r')"
if printf '%s' "$first" | grep -qi '<!doctype html\|<html\|sign in - gitea\|<title>sign in'; then
rm -f "$tmp" "$hdr"; _fs_html_trap_error "$url"; return 1
fi
case "$ctype" in
*text/html*) rm -f "$tmp" "$hdr"; _fs_html_trap_error "$url"; return 1 ;;
esac
rm -f "$hdr"
line1="$(head -1 "$tmp" 2>/dev/null | tr -d '\r')"
case "$kind" in
version)
local ver; ver="$(printf '%s' "$first" | tr -d '[:space:]')"
if ! printf '%s' "$ver" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+'; then
rm -f "$tmp"
printf 'error: %s — expected a semver VERSION (e.g. 0.8.4), got %s.\n' "$url" "$(_fs_snippet "$tmp" "$first")" >&2
return 1
fi ;;
manifest)
if printf '%s' "$first" | grep -q '<'; then
rm -f "$tmp"; printf 'error: %s — MANIFEST contains HTML markup ("<").\n' "$url" >&2; return 1
fi
# v0.8.11: accept "path<TAB>sha256" (hash group optional => legacy paths-
# only still matches). The '<' guard above is the HTML-trap defense.
if ! grep -Eq '^[A-Za-z0-9_][A-Za-z0-9_./-]*([[:space:]]+[0-9a-fA-F]{64})?[[:space:]]*$' "$tmp"; then
rm -f "$tmp"; printf 'error: %s — MANIFEST has no plausible path line.\n' "$url" >&2; return 1
fi ;;
script)
if [ "$line1" != '#!/usr/bin/env bash' ]; then
rm -f "$tmp"
printf 'error: %s — larry.sh must start with `#!/usr/bin/env bash`, got %s.\n' "$url" "$(_fs_snippet "$tmp" "$first")" >&2
return 1
fi ;;
sh|text|*) : ;;
esac
mkdir -p "$(dirname "$dest")" 2>/dev/null || true
mv "$tmp" "$dest" || { rm -f "$tmp"; printf 'error: cannot write %s\n' "$dest" >&2; return 1; }
return 0
}
# <<< fetch-safe inline <<<
# ─────────────────────────────────────────────────────────────────────────────
# Detect platform
# ─────────────────────────────────────────────────────────────────────────────
UNAME_S="$(uname -s 2>/dev/null || echo unknown)"
PLATFORM=""
case "$UNAME_S" in
Linux*) PLATFORM="linux" ;;
Darwin*) PLATFORM="darwin" ;;
CYGWIN*|MINGW*|MSYS*) PLATFORM="windows-cygwin" ;; # MobaXterm lives here
*) PLATFORM="unknown" ;;
esac
ARCH="$(uname -m 2>/dev/null || echo unknown)"
case "$ARCH" in
x86_64|amd64) ARCH_NORM="amd64" ;;
aarch64|arm64) ARCH_NORM="arm64" ;;
i?86) ARCH_NORM="i386" ;;
*) ARCH_NORM="$ARCH" ;;
esac
say "platform: $PLATFORM/$ARCH_NORM • LARRY_HOME=$LARRY_HOME"
# ─────────────────────────────────────────────────────────────────────────────
# Check required commands
# ─────────────────────────────────────────────────────────────────────────────
command -v bash >/dev/null 2>&1 || die "bash not found"
command -v curl >/dev/null 2>&1 || die "curl not found"
# ─────────────────────────────────────────────────────────────────────────────
# Make dirs
# ─────────────────────────────────────────────────────────────────────────────
mkdir -p "$LARRY_HOME"/{agents,sessions,bin,lib} || die "cannot create $LARRY_HOME"
chmod 700 "$LARRY_HOME" 2>/dev/null || true
ok "created $LARRY_HOME"
# ─────────────────────────────────────────────────────────────────────────────
# Fetch the scripts. If LARRY_BASE_URL is not set, try to detect being run
# from a local checkout (sibling files present) — copy locally instead.
# ─────────────────────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" 2>/dev/null && pwd)" || SCRIPT_DIR=""
# v0.7.4 single-source: install-larry.sh is the FIRST contact with the
# origin — it runs before larry.sh exists, so it must succeed in one shot.
# No fallback; if $LARRY_BASE_URL is unreachable we die with a clear error
# telling the user to verify the URL or set an alternate mirror.
# _kind_for REL — infer the content-shape contract for a manifest path so
# every fetch gets validated (HTML-trap + shape) before we trust the bytes.
_kind_for() {
case "$1" in
larry.sh) printf 'script' ;;
VERSION) printf 'version' ;;
MANIFEST) printf 'manifest' ;;
*.sh) printf 'sh' ;;
*) printf 'text' ;;
esac
}
fetch() {
# $1 = remote relative path, $2 = local destination
if [ -n "$LARRY_BASE_URL" ]; then
say "fetching $1"
# v0.8.4 hardening: validate every remote fetch (HTML-sign-in-page trap +
# content-shape) BEFORE trusting the bytes. fetch_validate writes $2 only
# on success; on failure it prints an actionable error and leaves $2
# untouched, so we never overwrite a real file with HTML soup.
if fetch_validate "$LARRY_BASE_URL/$1" "$2" "$(_kind_for "$1")" 30; then
ok "$2"
return 0
fi
rm -f "$2"
die "install failed: cannot fetch $1 from LARRY_BASE_URL=$LARRY_BASE_URL — see error above (verify the URL, repo visibility, or set LARRY_GITEA_TOKEN / a reachable mirror)"
elif [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/$1" ]; then
cp "$SCRIPT_DIR/$1" "$2" && ok "copied $1 (local)"
else
die "no LARRY_BASE_URL set and $1 not found in script dir"
fi
}
# Like fetch(), but NEVER overwrites an existing destination — used for the
# Bryan-curated inbound-systems lookup so a re-install/update preserves edits.
fetch_if_missing() {
if [ -f "$2" ]; then
ok "$2 (kept existing — user-curated)"
return 0
fi
fetch "$1" "$2"
}
fetch larry.sh "$LARRY_HOME/larry.sh"
fetch larry-tunnel.sh "$LARRY_HOME/larry-tunnel.sh"
fetch agents/larry.md "$LARRY_HOME/agents/larry.md"
fetch agents/clover.md "$LARRY_HOME/agents/clover.md"
fetch agents/cloverleaf-cheatsheet.md "$LARRY_HOME/agents/cloverleaf-cheatsheet.md"
fetch agents/regress.md "$LARRY_HOME/agents/regress.md"
fetch larry-rollback.sh "$LARRY_HOME/larry-rollback.sh"
fetch larry-auth.sh "$LARRY_HOME/larry-auth.sh"
fetch uninstall-larry.sh "$LARRY_HOME/uninstall-larry.sh"
fetch lib/fetch-safe.sh "$LARRY_HOME/lib/fetch-safe.sh"
fetch lib/cygwin-safe.sh "$LARRY_HOME/lib/cygwin-safe.sh"
fetch lib/oauth.sh "$LARRY_HOME/lib/oauth.sh"
fetch lib/broker.sh "$LARRY_HOME/lib/broker.sh"
fetch lib/ssh-helper.sh "$LARRY_HOME/lib/ssh-helper.sh"
fetch lib/lessons.sh "$LARRY_HOME/lib/lessons.sh"
fetch lib/hl7-sanitize.sh "$LARRY_HOME/lib/hl7-sanitize.sh"
fetch lib/hl7-desanitize.sh "$LARRY_HOME/lib/hl7-desanitize.sh"
fetch lib/each.sh "$LARRY_HOME/lib/each.sh"
fetch lib/each-site.sh "$LARRY_HOME/lib/each-site.sh"
fetch lib/len2nl.sh "$LARRY_HOME/lib/len2nl.sh"
fetch lib/csv-to-table.sh "$LARRY_HOME/lib/csv-to-table.sh"
fetch lib/table-to-csv.sh "$LARRY_HOME/lib/table-to-csv.sh"
fetch lib/nc-engine.sh "$LARRY_HOME/lib/nc-engine.sh"
fetch lib/nc-status.sh "$LARRY_HOME/lib/nc-status.sh"
fetch lib/nc-table.sh "$LARRY_HOME/lib/nc-table.sh"
fetch lib/nc-xlate.sh "$LARRY_HOME/lib/nc-xlate.sh"
fetch lib/nc-smat-diff.sh "$LARRY_HOME/lib/nc-smat-diff.sh"
fetch lib/nc-create-thread.sh "$LARRY_HOME/lib/nc-create-thread.sh"
fetch lib/nc-tclgen.sh "$LARRY_HOME/lib/nc-tclgen.sh"
fetch lib/nc-parse.sh "$LARRY_HOME/lib/nc-parse.sh"
fetch lib/nc-inbound.sh "$LARRY_HOME/lib/nc-inbound.sh"
fetch lib/nc-make-jump.sh "$LARRY_HOME/lib/nc-make-jump.sh"
fetch lib/hl7-field.sh "$LARRY_HOME/lib/hl7-field.sh"
fetch lib/hl7-schema.sh "$LARRY_HOME/lib/hl7-schema.sh"
fetch lib/nc-msgs.sh "$LARRY_HOME/lib/nc-msgs.sh"
fetch lib/nc-document.sh "$LARRY_HOME/lib/nc-document.sh"
fetch lib/nc-diff-interface.sh "$LARRY_HOME/lib/nc-diff-interface.sh"
fetch lib/nc-find.sh "$LARRY_HOME/lib/nc-find.sh"
fetch lib/nc-insert-protocol.sh "$LARRY_HOME/lib/nc-insert-protocol.sh"
fetch lib/hl7-diff.sh "$LARRY_HOME/lib/hl7-diff.sh"
fetch lib/nc-regression.sh "$LARRY_HOME/lib/nc-regression.sh"
fetch lib/journal.sh "$LARRY_HOME/lib/journal.sh"
# bin/ — short, directly-invokable command wrappers + dynamic tab-completion
# (v0.9.4). The full lib/ + bin/ set is reconciled by larry.sh self_update from
# MANIFEST on first launch; these explicit fetches make the short commands +
# completion available immediately, before the first `larry` launch.
fetch bin/_nc_common.sh "$LARRY_HOME/bin/_nc_common.sh"
fetch bin/nc-completion.bash "$LARRY_HOME/bin/nc-completion.bash"
for _w in help cheat tbn tbp tbh tbpr where paths route_test \
parse nfind inbound status engine xlate table msgs \
create-thread set-field insert-protocol make-jump provision-jumps \
tclgen document revisions diff-interface smat-diff regression \
nc-parse nc-paths nc-find nc-inbound nc-status nc-engine nc-xlate nc-table \
nc-create-thread nc-set-field nc-insert-protocol nc-make-jump nc-provision-jumps \
nc-tclgen nc-document nc-revisions nc-diff-interface nc-smat-diff nc-regression \
nc-msgs hl7-field hl7-diff len2nl hl7-sanitize hl7-desanitize each-site \
csv-to-table table-to-csv; do
fetch "bin/$_w" "$LARRY_HOME/bin/$_w"
done
chmod +x "$LARRY_HOME/bin/"* 2>/dev/null || true
fetch VERSION "$LARRY_HOME/VERSION"
fetch MANUAL.md "$LARRY_HOME/MANUAL.md"
# Bryan-curated inbound-systems lookup — seed only if absent (never clobber edits).
fetch_if_missing inbound-systems.tsv "$LARRY_HOME/inbound-systems.tsv"
chmod +x "$LARRY_HOME/larry.sh" "$LARRY_HOME/larry-tunnel.sh" "$LARRY_HOME/larry-rollback.sh" "$LARRY_HOME/larry-auth.sh" "$LARRY_HOME/uninstall-larry.sh" "$LARRY_HOME/lib/"*.sh
# ─────────────────────────────────────────────────────────────────────────────
# jq fallback — download static binary into $LARRY_HOME/bin/ if missing
# ─────────────────────────────────────────────────────────────────────────────
if ! command -v jq >/dev/null 2>&1 && [ ! -x "$LARRY_HOME/bin/jq" ]; then
say "jq not found — fetching static binary into $LARRY_HOME/bin/jq"
JQ_BASE="https://github.com/jqlang/jq/releases/download/jq-1.7.1"
JQ_URL=""
case "$PLATFORM/$ARCH_NORM" in
linux/amd64) JQ_URL="$JQ_BASE/jq-linux-amd64" ;;
linux/arm64) JQ_URL="$JQ_BASE/jq-linux-arm64" ;;
linux/i386) JQ_URL="$JQ_BASE/jq-linux-i386" ;;
darwin/amd64) JQ_URL="$JQ_BASE/jq-macos-amd64" ;;
darwin/arm64) JQ_URL="$JQ_BASE/jq-macos-arm64" ;;
windows-cygwin/amd64) JQ_URL="$JQ_BASE/jq-windows-amd64.exe" ;;
windows-cygwin/i386) JQ_URL="$JQ_BASE/jq-windows-i386.exe" ;;
*) warn "no jq binary known for $PLATFORM/$ARCH_NORM — install jq manually" ;;
esac
if [ -n "$JQ_URL" ]; then
local_jq="$LARRY_HOME/bin/jq"
case "$PLATFORM" in windows-cygwin) local_jq="$LARRY_HOME/bin/jq.exe" ;; esac
if curl -fsSL --max-time 60 "$JQ_URL" -o "$local_jq"; then
chmod +x "$local_jq"
ok "jq -> $local_jq"
else
warn "could not download jq from $JQ_URL"
fi
fi
else
ok "jq available"
fi
# ─────────────────────────────────────────────────────────────────────────────
# Drop a `larry` shim onto PATH (best-effort)
# ─────────────────────────────────────────────────────────────────────────────
mkdir -p "$LARRY_BIN_DIR" 2>/dev/null || true
if [ -d "$LARRY_BIN_DIR" ] && [ -w "$LARRY_BIN_DIR" ]; then
cat > "$LARRY_BIN_DIR/larry" <<EOF
#!/usr/bin/env bash
# Auto-generated by install-larry.sh
export LARRY_HOME="${LARRY_HOME}"
exec "${LARRY_HOME}/larry.sh" "\$@"
EOF
chmod +x "$LARRY_BIN_DIR/larry"
ok "shim: $LARRY_BIN_DIR/larry"
case ":$PATH:" in
*":$LARRY_BIN_DIR:"*) : ;;
*) warn "$LARRY_BIN_DIR is not on PATH — add 'export PATH=\"$LARRY_BIN_DIR:\$PATH\"' to your shell rc" ;;
esac
else
warn "cannot write to $LARRY_BIN_DIR — invoke larry directly as: $LARRY_HOME/larry.sh"
fi
# ─────────────────────────────────────────────────────────────────────────────
# Short, directly-invokable commands onto PATH + dynamic tab-completion (v0.9.4)
#
# Symlink every wrapper in $LARRY_HOME/bin/ into $LARRY_BIN_DIR so `tbn adt`,
# `paths`, `route_test`, `nc-find` … work DIRECTLY (no `larry tools` prefix).
# Symlinks (not copies) so a self_update of $LARRY_HOME/bin stays authoritative.
# _nc_common.sh + nc-completion.bash are NOT linked (sourced helpers, not cmds).
# ─────────────────────────────────────────────────────────────────────────────
if [ -d "$LARRY_BIN_DIR" ] && [ -w "$LARRY_BIN_DIR" ] && [ -d "$LARRY_HOME/bin" ]; then
_linked=0
for _t in "$LARRY_HOME/bin/"*; do
_b="$(basename "$_t")"
case "$_b" in _nc_common.sh|nc-completion.bash|jq|jq.exe) continue ;; esac
ln -sf "$_t" "$LARRY_BIN_DIR/$_b" 2>/dev/null && _linked=$((_linked+1))
done
ok "linked $_linked short commands onto PATH ($LARRY_BIN_DIR) — e.g. 'tbn adt', 'paths', 'route_test'"
fi
# Tab-completion: append a guarded source line to the user's bash rc (idempotent).
_comp="$LARRY_HOME/bin/nc-completion.bash"
if [ -f "$_comp" ]; then
_rc=""
for _cand in "$HOME/.bashrc" "$HOME/.bash_profile"; do
[ -f "$_cand" ] && { _rc="$_cand"; break; }
done
[ -z "$_rc" ] && _rc="$HOME/.bashrc"
_line="[ -f \"$_comp\" ] && source \"$_comp\" # cloverleaf-larry tab-completion"
if ! grep -qF "$_comp" "$_rc" 2>/dev/null; then
printf '\n%s\n' "$_line" >> "$_rc" && ok "tab-completion enabled in $_rc (open a new shell or: source $_comp)"
else
ok "tab-completion already wired in $_rc"
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# v0.8.2 — optional PHI Presidio sidecar (free-text NER).
# Closes V1 from Vera's PHI-leak audit. Opt-in install; larry runs in
# v0.8.1 mode (rule-pack only) on hosts where this isn't installed.
# We probe for python3 + pip, then offer the install. Skip silently if
# python3 isn't available — keeps the install one-shot on raw MobaXterm
# where Python may not be present.
# ─────────────────────────────────────────────────────────────────────────────
if command -v python3 >/dev/null 2>&1; then
PYV=$(python3 -c 'import sys; print(f"{sys.version_info.major}.{sys.version_info.minor}")' 2>/dev/null || echo "")
case "$PYV" in
3.9|3.10|3.11|3.12|3.13|3.14|3.15) PY_OK=1 ;;
*) PY_OK=0 ;;
esac
if [ "${PY_OK:-0}" = "1" ]; then
say "v0.8.2: Presidio PHI sidecar is available (python $PYV detected)"
echo " Presidio provides free-text NER (names, addresses, dates in prose)"
echo " that the regex tiers miss. Install adds presidio_analyzer +"
echo " presidio_anonymizer + fastapi + uvicorn + spaCy en_core_web_sm"
echo " to a dedicated virtualenv at $LARRY_HOME/phi-venv (~400MB on disk,"
echo " ~250MB RAM resident when running). One-time cost; tier-5 NER"
echo " then runs on every prompt with ~20ms latency."
echo ""
# Heuristic: if stdin is a TTY, prompt. Otherwise (curl|bash pipe), skip.
INSTALL_PHI=""
if [ -t 0 ]; then
printf 'install Presidio sidecar now? [y/N]: '
read -r INSTALL_PHI </dev/tty || INSTALL_PHI=""
else
echo " (non-interactive install — skip; rerun installer with TTY or set"
echo " LARRY_INSTALL_PHI=1 to enable. To install manually later:"
echo " python3 -m venv $LARRY_HOME/phi-venv"
echo " $LARRY_HOME/phi-venv/bin/pip install presidio_analyzer presidio_anonymizer fastapi uvicorn"
echo " $LARRY_HOME/phi-venv/bin/python -m spacy download en_core_web_sm)"
INSTALL_PHI="${LARRY_INSTALL_PHI:-n}"
fi
case "${INSTALL_PHI:-}" in
y|Y|yes|YES|1)
say "installing Presidio sidecar to $LARRY_HOME/phi-venv (this takes 2-5 minutes)..."
if python3 -m venv "$LARRY_HOME/phi-venv" >/dev/null 2>&1; then
if "$LARRY_HOME/phi-venv/bin/pip" install --quiet \
presidio_analyzer presidio_anonymizer fastapi uvicorn >/dev/null 2>&1; then
if "$LARRY_HOME/phi-venv/bin/python" -m spacy download en_core_web_sm \
>/dev/null 2>&1; then
ok "Presidio sidecar installed (venv: $LARRY_HOME/phi-venv)"
# Set LARRY_PHI_VENV in the shim so larry auto-uses it.
if [ -f "$LARRY_BIN_DIR/larry" ]; then
sed -i.bak "s|^exec \"|export LARRY_PHI_VENV=\"$LARRY_HOME/phi-venv\"\nexec \"|" \
"$LARRY_BIN_DIR/larry" 2>/dev/null || true
rm -f "$LARRY_BIN_DIR/larry.bak"
fi
else
warn "spaCy en_core_web_sm download failed; sidecar will not start until model is present"
fi
else
warn "pip install failed; Presidio sidecar not available on this host (larry runs in v0.8.1 mode)"
fi
else
warn "python3 -m venv failed; cannot install Presidio (larry runs in v0.8.1 mode)"
fi
;;
*)
ok "skipped Presidio install — larry runs in v0.8.1 mode (rule-pack auto-PHI only)"
;;
esac
else
warn "python3 detected but version ($PYV) is not 3.9+; Presidio sidecar requires 3.9+"
warn "larry runs in v0.8.1 mode (rule-pack auto-PHI only) on this host"
fi
else
case "$PLATFORM" in
windows-cygwin)
warn "python3 not detected on Cygwin/MobaXterm. v0.8.2 Presidio sidecar SKIPPED."
warn "Bryan's accepted tradeoff: MobaXterm stays on v0.8.1 + prompt nudges."
;;
*)
warn "python3 not on PATH; Presidio sidecar skipped (larry runs in v0.8.1 mode)"
;;
esac
fi
# ─────────────────────────────────────────────────────────────────────────────
# v0.9.0 — BROKER ENROLLMENT (the remote kill-switch; DEFAULT for every deploy).
#
# Broker mode needs two facts baked per-box: a deployment id (its row in the
# broker dashboard) and the enrollment secret (exchanged for short-lived tokens).
# Bryan provisions them ONCE per box via env at install time:
#
# docker exec larry-broker python /app/broker_ctl.py enroll gundersen-epic \
# --client "Gundersen" --profile phi --note "Epic interface box"
# # copy the printed secret, then on the target box:
# LARRY_DEPLOYMENT_ID=gundersen-epic \
# LARRY_ENROLL_SECRET=<paste> \
# LARRY_PROFILE=phi \
# LARRY_BROKER_URL=http://100.86.16.114:8181 \
# bash install-larry.sh
#
# The id+profile show up in Iris's dashboard ready to toggle. The secret is
# stored 0600 at $LARRY_HOME/.enroll-secret and NEVER printed/committed. If the
# operator skips enrollment, the box still installs but broker-mode will fail-
# closed on first launch (no secret) — they must enroll, or deliberately opt out
# with LARRY_AUTH_MODE=apikey (escape hatch, NO kill-switch — not for PHI boxes).
# ─────────────────────────────────────────────────────────────────────────────
BROKER_ENROLLED=0
if [ -n "${LARRY_DEPLOYMENT_ID:-}" ] || [ -n "${LARRY_ENROLL_SECRET:-}" ]; then
if [ -z "${LARRY_DEPLOYMENT_ID:-}" ]; then
warn "LARRY_ENROLL_SECRET given without LARRY_DEPLOYMENT_ID — skipping enrollment."
elif [ -z "${LARRY_ENROLL_SECRET:-}" ]; then
warn "LARRY_DEPLOYMENT_ID given without LARRY_ENROLL_SECRET — skipping enrollment."
else
umask 077
printf '%s\n' "${LARRY_DEPLOYMENT_ID//$'\r'/}" > "$LARRY_HOME/.deployment-id"
printf '%s\n' "${LARRY_ENROLL_SECRET//$'\r'/}" > "$LARRY_HOME/.enroll-secret"
chmod 600 "$LARRY_HOME/.deployment-id" "$LARRY_HOME/.enroll-secret" 2>/dev/null || true
BROKER_ENROLLED=1
ok "broker-enrolled as deployment '${LARRY_DEPLOYMENT_ID}' (profile=${LARRY_PROFILE:-default})"
ok " enrollment secret stored 0600 at $LARRY_HOME/.enroll-secret (never printed)"
fi
fi
# Bake the broker config into the `larry` shim so EVERY launch is broker-mode
# (the kill-switch is wired in by default, not something the operator has to
# remember to export). The shim already sets LARRY_HOME; we prepend the broker
# env. LARRY_AUTH_MODE is left to default to broker in larry.sh — but we pin the
# id/profile/broker-url here so the box phones the right broker under the right
# name. (apikey opt-out: re-run with LARRY_AUTH_MODE=apikey or edit this shim.)
if [ -f "$LARRY_BIN_DIR/larry" ]; then
_shim_inject=""
[ -n "${LARRY_BROKER_URL:-}" ] && _shim_inject="${_shim_inject}export LARRY_BROKER_URL=\"${LARRY_BROKER_URL}\"\n"
[ -n "${LARRY_DEPLOYMENT_ID:-}" ] && _shim_inject="${_shim_inject}export LARRY_DEPLOYMENT_ID=\"${LARRY_DEPLOYMENT_ID}\"\n"
[ -n "${LARRY_PROFILE:-}" ] && _shim_inject="${_shim_inject}export LARRY_PROFILE=\"${LARRY_PROFILE}\"\n"
if [ -n "$_shim_inject" ]; then
# Insert the exports just before the final `exec` line of the shim.
awk -v inj="$_shim_inject" '
/^exec / && !done { printf "%s", inj; done=1 } { print }
' "$LARRY_BIN_DIR/larry" > "$LARRY_BIN_DIR/larry.tmp" 2>/dev/null \
&& mv "$LARRY_BIN_DIR/larry.tmp" "$LARRY_BIN_DIR/larry" \
&& chmod +x "$LARRY_BIN_DIR/larry" \
&& ok "broker config baked into shim $LARRY_BIN_DIR/larry"
rm -f "$LARRY_BIN_DIR/larry.tmp" 2>/dev/null || true
fi
fi
# ─────────────────────────────────────────────────────────────────────────────
# Done
# ─────────────────────────────────────────────────────────────────────────────
echo ""
say "install complete (no system changes were made; everything lives under $LARRY_HOME)"
say "origin (single-source, v0.7.4): $LARRY_BASE_URL"
echo ""
echo "Next steps:"
if [ "$BROKER_ENROLLED" = "1" ]; then
echo " AUTH: broker mode (DEFAULT) — NO API key on this box. The kill-switch is ON."
echo " deployment '${LARRY_DEPLOYMENT_ID}' will appear in the dashboard, ready to toggle."
echo " REACHABILITY: the broker is LAN + Tailscale only (no public route). This box MUST"
echo " reach ${LARRY_BROKER_URL:-the broker} — over Tailscale on an egress-restricted network."
echo " If it cannot reach the broker, larry FAIL-CLOSES (refuses to run). That is a correct"
echo " kill state but a useless working state — bring Tailscale up first."
echo " 1) larry (or $LARRY_HOME/larry.sh)"
echo " 2) larry /path/to/cloverleaf/site_root (to start with a working dir)"
else
echo " AUTH: broker mode is the DEFAULT but this box was NOT enrolled (no LARRY_DEPLOYMENT_ID/"
echo " LARRY_ENROLL_SECRET given). larry will FAIL-CLOSED on launch until you either:"
echo " (a) enroll: docker exec larry-broker python /app/broker_ctl.py enroll <id> ... ,"
echo " then re-run install with LARRY_DEPLOYMENT_ID/LARRY_ENROLL_SECRET/LARRY_PROFILE; or"
echo " (b) opt out of the kill-switch (NOT for PHI boxes): export LARRY_AUTH_MODE=apikey"
echo " and run /set-api-key."
echo " 1) larry (or $LARRY_HOME/larry.sh)"
fi
echo ""
echo "Reverse SSH tunnel (optional, run in another shell or backgrounded):"
echo " $LARRY_HOME/larry-tunnel.sh --serveo # zero-config"
echo " $LARRY_HOME/larry-tunnel.sh --hop=user@bjnoela.com:22 # your hop"
echo ""
echo "To remove EVERYTHING this installed (dry-run first, then --yes):"
echo " larry uninstall # or: $LARRY_HOME/uninstall-larry.sh"
echo " larry uninstall --yes # actually delete"
echo ""