#!/usr/bin/env bash # ───────────────────────────────────────────────────────────────────────────── # larry-anywhere v0.8.2: PHI Presidio sidecar lifecycle # # Manages the local Presidio FastAPI service used by auto-PHI tier-5 # (free-text NER). Started once at larry-anywhere REPL boot (best-effort — # never blocks larry's startup), reused across turns, torn down on exit. # # Subcommands: # start — launch the sidecar in the background if not already up # stop — gracefully terminate the sidecar (TERM, then KILL) # status — report up/down + port + pid # health — curl /health endpoint (one-shot) # ensure — start if not up; quick no-op if up. Idempotent. The # primary entry point for larry.sh launch flow. # # Env: # LARRY_PHI_PORT default 41189 # LARRY_PHI_HOST default 127.0.0.1 # LARRY_PHI_PYTHON default python3 # LARRY_PHI_VENV optional path to a virtualenv; if set, uses # $LARRY_PHI_VENV/bin/python instead # LARRY_HOME stores PID file at $LARRY_HOME/.phi-sidecar.pid # and stderr log at $LARRY_HOME/log/phi-sidecar.log # # Failure handling: # If the sidecar can't start (missing deps, port collision, model missing), # `start` returns non-zero with a stderr explanation. Callers in larry.sh # MUST treat sidecar absence as "tier-5 disabled" — don't block the turn. # ───────────────────────────────────────────────────────────────────────────── set -uo pipefail LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" LARRY_PHI_PORT="${LARRY_PHI_PORT:-41189}" LARRY_PHI_HOST="${LARRY_PHI_HOST:-127.0.0.1}" LARRY_PHI_PYTHON="${LARRY_PHI_PYTHON:-python3}" _PHI_SCRIPT_DIR="$(cd "$(dirname "$0")" 2>/dev/null && pwd)" _PHI_SIDECAR_PY="$_PHI_SCRIPT_DIR/phi-presidio-sidecar.py" _PHI_PID_FILE="$LARRY_HOME/.phi-sidecar.pid" _PHI_LOG_FILE="$LARRY_HOME/log/phi-sidecar.log" # Coerce CR-tainted port number (Cygwin defense — v0.7.5 lesson). if [ -f "$_PHI_SCRIPT_DIR/cygwin-safe.sh" ]; then # shellcheck source=cygwin-safe.sh . "$_PHI_SCRIPT_DIR/cygwin-safe.sh" 2>/dev/null || true fi if declare -F coerce_int >/dev/null 2>&1; then LARRY_PHI_PORT=$(coerce_int "$LARRY_PHI_PORT" 41189) fi _phi_python() { if [ -n "${LARRY_PHI_VENV:-}" ] && [ -x "$LARRY_PHI_VENV/bin/python" ]; then printf '%s' "$LARRY_PHI_VENV/bin/python" return fi if command -v "$LARRY_PHI_PYTHON" >/dev/null 2>&1; then printf '%s' "$LARRY_PHI_PYTHON" return fi printf '' } _phi_is_up() { # Health check via curl (lightweight). Don't trust the PID file alone — # process could be a stale pid for an unrelated python. curl -fsS -m 1 "http://${LARRY_PHI_HOST}:${LARRY_PHI_PORT}/health" >/dev/null 2>&1 } cmd_status() { if _phi_is_up; then local body; body=$(curl -fsS -m 1 "http://${LARRY_PHI_HOST}:${LARRY_PHI_PORT}/health" 2>/dev/null) printf 'phi-sidecar: up — %s (pid %s)\n' "$body" "$(cat "$_PHI_PID_FILE" 2>/dev/null || echo unknown)" return 0 fi printf 'phi-sidecar: down\n' return 1 } cmd_health() { curl -fsS -m 1 "http://${LARRY_PHI_HOST}:${LARRY_PHI_PORT}/health" 2>/dev/null local rc=$? if [ "$rc" != "0" ]; then printf '{"status":"down","error":"unreachable on %s:%s"}\n' "$LARRY_PHI_HOST" "$LARRY_PHI_PORT" >&2 return 1 fi echo return 0 } cmd_start() { if _phi_is_up; then cmd_status return 0 fi local py; py=$(_phi_python) if [ -z "$py" ]; then printf 'phi-sidecar: cannot start — python3 not on PATH (set LARRY_PHI_PYTHON or LARRY_PHI_VENV)\n' >&2 return 4 fi if [ ! -r "$_PHI_SIDECAR_PY" ]; then printf 'phi-sidecar: cannot start — %s missing\n' "$_PHI_SIDECAR_PY" >&2 return 4 fi # Quick dependency probe (don't load the model — that takes 9s. Just # check imports succeed). If this fails, exit early with a clear message. if ! "$py" -c 'import presidio_analyzer, presidio_anonymizer, fastapi, uvicorn' 2>/dev/null; then printf 'phi-sidecar: cannot start — presidio_analyzer / presidio_anonymizer / fastapi / uvicorn not installed for %s\n' "$py" >&2 printf ' install with: %s -m pip install presidio_analyzer presidio_anonymizer fastapi uvicorn\n' "$py" >&2 printf ' then: %s -m spacy download en_core_web_sm\n' "$py" >&2 return 5 fi mkdir -p "$(dirname "$_PHI_PID_FILE")" "$(dirname "$_PHI_LOG_FILE")" 2>/dev/null LARRY_PHI_PORT="$LARRY_PHI_PORT" LARRY_PHI_HOST="$LARRY_PHI_HOST" \ nohup "$py" "$_PHI_SIDECAR_PY" >> "$_PHI_LOG_FILE" 2>&1 & local pid=$! echo "$pid" > "$_PHI_PID_FILE" # Wait up to 30 seconds for the model to load + the FastAPI port to open. local i for i in $(seq 1 30); do sleep 1 if _phi_is_up; then printf 'phi-sidecar: started in %ds (pid %s, port %s)\n' "$i" "$pid" "$LARRY_PHI_PORT" >&2 return 0 fi # If the python process died, surface the tail of the log. if ! kill -0 "$pid" 2>/dev/null; then printf 'phi-sidecar: process died during startup; tail of log:\n' >&2 tail -20 "$_PHI_LOG_FILE" >&2 rm -f "$_PHI_PID_FILE" return 6 fi done printf 'phi-sidecar: did not become healthy within 30s; tail of log:\n' >&2 tail -20 "$_PHI_LOG_FILE" >&2 return 7 } cmd_stop() { local pid="" [ -f "$_PHI_PID_FILE" ] && pid=$(cat "$_PHI_PID_FILE" 2>/dev/null) if [ -z "$pid" ]; then printf 'phi-sidecar: no pid file\n' return 0 fi if kill -0 "$pid" 2>/dev/null; then kill -TERM "$pid" 2>/dev/null local i for i in 1 2 3 4 5; do sleep 1 kill -0 "$pid" 2>/dev/null || break done if kill -0 "$pid" 2>/dev/null; then kill -KILL "$pid" 2>/dev/null fi fi rm -f "$_PHI_PID_FILE" printf 'phi-sidecar: stopped (pid %s)\n' "$pid" } cmd_ensure() { if _phi_is_up; then return 0 fi cmd_start } case "${1:-}" in start) shift; cmd_start "$@" ;; stop) shift; cmd_stop "$@" ;; status) shift; cmd_status "$@" ;; health) shift; cmd_health "$@" ;; ensure) shift; cmd_ensure "$@" ;; ""|help|-h|--help) cat <&2; exit 2 ;; esac