#!/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 /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= 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 '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 "pathsha256" (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" 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" </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/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= \ # 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 ... ," 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 ""