cloverleaf-larry/larry-tunnel.sh
Bryan Johnson e08f030df5 v0.3.0: initial release of Larry-Anywhere
Portable AI agent for Cloverleaf integration work. Pure bash + curl + jq.
Zero dependency on v1 wrapper scripts or v2 cloverleaf-tools.pyz.

27 native Anthropic tools:

NetConfig parsing (read)
  nc_list_protocols, nc_list_processes, nc_protocol_block,
  nc_protocol_field, nc_protocol_nested, nc_protocol_summary,
  nc_destinations, nc_sources, nc_xlate_refs, nc_tclproc_refs

NetConfig modification (journal-backed writes with rollback)
  nc_insert_protocol, nc_add_route, larry_rollback_list

Workflows
  nc_find_inbound, nc_make_jump (3-thread jump pattern), nc_find
  (tbn/tbp/tbh/tbpr/where replacements), nc_document, nc_diff_interface,
  nc_regression

Messages
  hl7_field, nc_msgs (smat is SQLite!), hl7_diff (with --ignore MSH.7)

File system
  read_file, list_dir, grep_files, glob_files, write_file, bash_exec

Validated against a 22-site real Cloverleaf test install. Five worked
examples end-to-end: jump-thread generation, smat MRN search, system
documentation, interface+connected diff, HL7-aware regression diff.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-26 09:46:20 -07:00

184 lines
6.1 KiB
Bash
Executable File

#!/usr/bin/env bash
# larry-tunnel — open a reverse SSH tunnel from a remote shell back to
# a public hop, so Bryan's home Larry can SSH "in" to this machine.
#
# Two modes:
# 1. Bryan-controlled hop (recommended once set up)
# Requires: an SSH-reachable host you control (bjnoela.com or a VPS)
# with `GatewayPorts clientspecified` (or yes) in sshd_config, plus
# either your SSH pubkey installed there or password auth allowed.
#
# 2. serveo.net fallback (zero-config, no account, no install)
# Just works. Less private. Useful when the primary hop is offline
# or not yet configured.
#
# Usage:
# larry-tunnel.sh # uses env vars or prompts
# larry-tunnel.sh --serveo # force serveo.net mode
# larry-tunnel.sh --hop user@host:port # force custom-hop mode
#
# Env vars (set in $LARRY_HOME/.env or shell):
# LARRY_HOP_HOST e.g. bjnoela.com
# LARRY_HOP_USER e.g. larry-tunnel (a low-priv user on the hop)
# LARRY_HOP_PORT SSH port on the hop (default 22)
# LARRY_HOP_BIND remote-side bind port (default 0 = auto)
# LARRY_LOCAL_PORT local SSH port to expose (default 22)
# LARRY_HOP_KEY path to private key for the hop (default ~/.ssh/id_rsa or id_ed25519)
#
# Files:
# $LARRY_HOME/tunnel.pid PID of the active tunnel ssh process
# $LARRY_HOME/tunnel.url the public host:port to dial from home
# $LARRY_HOME/tunnel.log ssh stderr/log
set -u
set -o pipefail
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
mkdir -p "$LARRY_HOME"
[ -f "$LARRY_HOME/.env" ] && { set -a; . "$LARRY_HOME/.env"; set +a; }
LARRY_HOP_HOST="${LARRY_HOP_HOST:-}"
LARRY_HOP_USER="${LARRY_HOP_USER:-}"
LARRY_HOP_PORT="${LARRY_HOP_PORT:-22}"
LARRY_HOP_BIND="${LARRY_HOP_BIND:-0}"
LARRY_LOCAL_PORT="${LARRY_LOCAL_PORT:-22}"
LARRY_HOP_KEY="${LARRY_HOP_KEY:-}"
C_RESET=$'\033[0m'; C_BOLD=$'\033[1m'; C_DIM=$'\033[2m'
C_RED=$'\033[31m'; C_GREEN=$'\033[32m'; C_YELLOW=$'\033[33m'; C_CYAN=$'\033[36m'
say() { printf '%s%slarry-tunnel>%s %s\n' "$C_CYAN" "$C_BOLD" "$C_RESET" "$*"; }
err() { printf '%serror:%s %s\n' "$C_RED" "$C_RESET" "$*" >&2; }
warn() { printf '%swarn:%s %s\n' "$C_YELLOW" "$C_RESET" "$*" >&2; }
MODE=""
for arg in "$@"; do
case "$arg" in
--serveo) MODE="serveo" ;;
--hop) MODE="hop" ;;
--hop=*) MODE="hop"; spec="${arg#--hop=}"
LARRY_HOP_USER="${spec%@*}"
rest="${spec#*@}"
LARRY_HOP_HOST="${rest%:*}"
[ "$rest" != "${rest%:*}" ] && LARRY_HOP_PORT="${rest##*:}"
;;
--status)
if [ -f "$LARRY_HOME/tunnel.pid" ] && kill -0 "$(cat "$LARRY_HOME/tunnel.pid")" 2>/dev/null; then
say "running (PID $(cat "$LARRY_HOME/tunnel.pid"))"
[ -f "$LARRY_HOME/tunnel.url" ] && say "public: $(cat "$LARRY_HOME/tunnel.url")"
exit 0
else
say "not running"
exit 1
fi
;;
--stop)
if [ -f "$LARRY_HOME/tunnel.pid" ]; then
kill "$(cat "$LARRY_HOME/tunnel.pid")" 2>/dev/null && say "stopped"
rm -f "$LARRY_HOME/tunnel.pid" "$LARRY_HOME/tunnel.url"
fi
exit 0
;;
-h|--help)
sed -n '2,32p' "$0"; exit 0
;;
*) err "unknown arg: $arg"; exit 2 ;;
esac
done
# Pick mode if not forced
if [ -z "$MODE" ]; then
if [ -n "$LARRY_HOP_HOST" ] && [ -n "$LARRY_HOP_USER" ]; then
MODE="hop"
else
MODE="serveo"
fi
fi
# Identify ssh key flag (only if set and exists)
KEY_FLAG=""
if [ -n "$LARRY_HOP_KEY" ] && [ -f "$LARRY_HOP_KEY" ]; then
KEY_FLAG="-i $LARRY_HOP_KEY"
fi
# Common ssh options for reverse tunnel
SSH_OPTS=(
-N
-o ServerAliveInterval=30
-o ServerAliveCountMax=3
-o ExitOnForwardFailure=yes
-o StrictHostKeyChecking=accept-new
-o UserKnownHostsFile="$LARRY_HOME/known_hosts"
)
run_hop() {
if [ -z "$LARRY_HOP_HOST" ] || [ -z "$LARRY_HOP_USER" ]; then
err "hop mode needs LARRY_HOP_HOST and LARRY_HOP_USER (or --hop=user@host)"
exit 2
fi
say "opening reverse tunnel: ${LARRY_HOP_USER}@${LARRY_HOP_HOST}:${LARRY_HOP_PORT}"
say " binds remote port ${LARRY_HOP_BIND} -> localhost:${LARRY_LOCAL_PORT}"
while true; do
: > "$LARRY_HOME/tunnel.log"
# shellcheck disable=SC2086
ssh $KEY_FLAG \
"${SSH_OPTS[@]}" \
-p "$LARRY_HOP_PORT" \
-R "${LARRY_HOP_BIND}:localhost:${LARRY_LOCAL_PORT}" \
"${LARRY_HOP_USER}@${LARRY_HOP_HOST}" \
2> "$LARRY_HOME/tunnel.log" &
local pid=$!
echo "$pid" > "$LARRY_HOME/tunnel.pid"
printf '%s:%s\n' "$LARRY_HOP_HOST" "$LARRY_HOP_BIND" > "$LARRY_HOME/tunnel.url"
say "ssh PID $pid — dial from home: ssh -p ${LARRY_HOP_BIND} <local-user>@${LARRY_HOP_HOST}"
wait "$pid"
rc=$?
warn "tunnel exited (rc=$rc) — reconnecting in 5s"
sleep 5
done
}
run_serveo() {
say "opening serveo.net reverse tunnel (no account needed)"
say " binds an auto-assigned port -> localhost:${LARRY_LOCAL_PORT}"
say " serveo.net is third-party; do NOT use for sensitive sessions"
while true; do
: > "$LARRY_HOME/tunnel.log"
# serveo prints assignment on stderr/stdout — we capture both
ssh \
"${SSH_OPTS[@]}" \
-R "0:localhost:${LARRY_LOCAL_PORT}" \
serveo.net \
> "$LARRY_HOME/tunnel.log" 2>&1 &
local pid=$!
echo "$pid" > "$LARRY_HOME/tunnel.pid"
# Try to extract the forwarded URL
local attempts=0 url=""
while [ "$attempts" -lt 30 ] && [ -z "$url" ]; do
sleep 1
attempts=$((attempts + 1))
url=$(grep -Eo 'Forwarding tcp from [^ ]+|tcp://[^ ]+|serveo\.net:[0-9]+' \
"$LARRY_HOME/tunnel.log" 2>/dev/null | head -1)
done
if [ -n "$url" ]; then
echo "$url" > "$LARRY_HOME/tunnel.url"
say "public: $url"
say "dial from home: ssh -p <PORT> <local-user>@serveo.net (PORT from URL above)"
else
warn "could not read serveo.net assignment — check $LARRY_HOME/tunnel.log"
fi
wait "$pid"
rc=$?
warn "tunnel exited (rc=$rc) — reconnecting in 5s"
sleep 5
done
}
trap 'kill $(cat "$LARRY_HOME/tunnel.pid" 2>/dev/null) 2>/dev/null; rm -f "$LARRY_HOME/tunnel.pid" "$LARRY_HOME/tunnel.url"; exit 0' INT TERM
case "$MODE" in
hop) run_hop ;;
serveo) run_serveo ;;
*) err "unknown mode: $MODE"; exit 2 ;;
esac