v0.5.0: MANIFEST-driven self-update + OAuth code#state parsing

Self-update overhaul (no more manual reinstalls when lib/ changes):
- New MANIFEST file at repo root lists every file that should auto-sync
  (top-level scripts, agents/, lib/, VERSION, MANUAL.md).
- larry.sh self_update() reworked into two phases:
    Phase A — local sync: if $LARRY_HOME/.last-sync-version != $LARRY_VERSION,
      fetch MANIFEST and refresh every listed file. Stamps version after.
    Phase B — remote check: fetch $LARRY_BASE_URL/VERSION; if newer, pull
      larry.sh, self-replace, relaunch with LARRY_JUST_UPDATED=1 so phase B
      is skipped on the relaunch (phase A then pulls everything else).
- New LARRY_BASE_URL env var (the legacy LARRY_UPDATE_URL / LARRY_AGENTS_URL
  still work as overrides).
- Bumped LARRY_VERSION and VERSION to 0.5.0.

OAuth fix (lib/oauth.sh):
- Anthropic's callback returns the code as 'CODE#STATE' (URL fragment, not
  query). Previous prompt told users to copy "between code= and the next &"
  which produced the wrong substring; the token endpoint then returned a
  misleading 'rate_limit_error' on the malformed code.
- Now splits the pasted input on '#', verifies the returned state matches
  the one we generated, sends only CODE to the token endpoint.
- Updated user-facing prompt and error hints to describe the real format
  and explain the misleading rate_limit_error symptom.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Bryan Johnson 2026-05-27 08:50:46 -07:00
parent b141d54847
commit 28622ca40b
4 changed files with 203 additions and 42 deletions

62
MANIFEST Normal file
View File

@ -0,0 +1,62 @@
# larry-anywhere update manifest
# Format: one path per line, relative to the bundle root.
# Lines starting with '#' and blank lines are ignored.
# Every file listed here is auto-synced by larry.sh's self_update() each time
# the running larry.sh version changes (and on first launch of a new version).
#
# To add a new file to the auto-sync set: list it here and bump VERSION.
# Top-level scripts
larry.sh
larry-tunnel.sh
larry-auth.sh
larry-rollback.sh
install-larry.sh
# Metadata
VERSION
MANUAL.md
# Agent personas (system-prompt overlays)
agents/larry.md
agents/clover.md
agents/cloverleaf-cheatsheet.md
agents/regress.md
# Auth implementation
lib/oauth.sh
# Logging / capture
lib/lessons.sh
lib/journal.sh
# HL7 utilities
lib/hl7-sanitize.sh
lib/hl7-desanitize.sh
lib/hl7-diff.sh
lib/hl7-field.sh
# Generic helpers
lib/each.sh
lib/each-site.sh
lib/len2nl.sh
lib/csv-to-table.sh
lib/table-to-csv.sh
# NetConfig tooling
lib/nc-engine.sh
lib/nc-status.sh
lib/nc-table.sh
lib/nc-xlate.sh
lib/nc-smat-diff.sh
lib/nc-create-thread.sh
lib/nc-tclgen.sh
lib/nc-parse.sh
lib/nc-inbound.sh
lib/nc-make-jump.sh
lib/nc-msgs.sh
lib/nc-document.sh
lib/nc-diff-interface.sh
lib/nc-find.sh
lib/nc-insert-protocol.sh
lib/nc-regression.sh

View File

@ -1 +1 @@
0.4.3
0.5.0

141
larry.sh
View File

@ -11,8 +11,12 @@
#
# Env vars:
# LARRY_HOME where to cache config/sessions (default: ~/.larry)
# LARRY_UPDATE_URL URL of latest larry.sh for self-update (optional)
# LARRY_AGENTS_URL base URL for agents/ refresh (optional)
# LARRY_BASE_URL root URL of the bundle on the server (default:
# https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main)
# Self-update pulls VERSION + MANIFEST from here and
# refreshes every file listed in MANIFEST.
# LARRY_UPDATE_URL (legacy override) full URL of latest larry.sh
# LARRY_AGENTS_URL (legacy override) base URL for agents/
# LARRY_MODEL Claude model (default: claude-sonnet-4-6)
# LARRY_MAX_TOKENS max output tokens per turn (default: 8192)
# LARRY_NO_UPDATE set to 1 to disable self-update
@ -32,10 +36,11 @@ set -o pipefail
# ─────────────────────────────────────────────────────────────────────────────
# Config
# ─────────────────────────────────────────────────────────────────────────────
LARRY_VERSION="0.4.3"
LARRY_VERSION="0.5.0"
LARRY_HOME="${LARRY_HOME:-$HOME/.larry}"
LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/larry.sh}"
LARRY_AGENTS_URL="${LARRY_AGENTS_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main/agents}"
LARRY_BASE_URL="${LARRY_BASE_URL:-https://raw.githubusercontent.com/bojj27/cloverleaf-larry/main}"
LARRY_UPDATE_URL="${LARRY_UPDATE_URL:-${LARRY_BASE_URL}/larry.sh}"
LARRY_AGENTS_URL="${LARRY_AGENTS_URL:-${LARRY_BASE_URL}/agents}"
LARRY_MODEL="${LARRY_MODEL:-claude-sonnet-4-6}"
LARRY_MAX_TOKENS="${LARRY_MAX_TOKENS:-8192}"
LARRY_API_URL="${LARRY_API_URL:-https://api.anthropic.com/v1/messages}"
@ -222,38 +227,114 @@ AGENT_EOF
fetch_agents_or_warn
# ─────────────────────────────────────────────────────────────────────────────
# Self-update
# Self-update — two-phase MANIFEST-driven sync.
#
# Phase A (local sync, no network if up-to-date):
# If $LARRY_HOME/.last-sync-version != $LARRY_VERSION, the running larry.sh
# is newer than the on-disk lib/agents/etc. files. Fetch MANIFEST from
# $LARRY_BASE_URL and refresh every file listed. Stamp .last-sync-version.
#
# Phase B (remote version check):
# Fetch $LARRY_BASE_URL/VERSION. If remote > local, pull new larry.sh,
# replace self, relaunch with LARRY_JUST_UPDATED=1 so phase B is skipped
# on the relaunch (avoids infinite loop). Phase A on the relaunch then
# pulls every other file matching the new version.
#
# Skip all of it via --no-update or LARRY_NO_UPDATE=1.
# ─────────────────────────────────────────────────────────────────────────────
self_update() {
[ "$LARRY_NO_UPDATE" = "1" ] && return 0
[ -z "$LARRY_UPDATE_URL" ] && return 0
sync_from_manifest() {
local base="$1"
local manifest="$LARRY_HOME/.manifest.new"
curl -fsSL --max-time 10 "$base/MANIFEST" -o "$manifest" 2>/dev/null || {
rm -f "$manifest"
return 1
}
[ -s "$manifest" ] || { rm -f "$manifest"; return 1; }
local self="$0"
case "$self" in /*) ;; *) self="$PWD/$self" ;; esac
local count=0 updated=0 failed=0 path tmp dest
while IFS= read -r path; do
case "$path" in ''|'#'*) continue ;; esac
path="${path%%[[:space:]]*}" # strip trailing whitespace/comments
[ -z "$path" ] && continue
count=$((count + 1))
# larry.sh is updated by phase B, not here — skip to avoid clobbering
# the running script mid-execution.
[ "$path" = "larry.sh" ] && continue
dest="$LARRY_HOME/$path"
tmp="$dest.new"
mkdir -p "$(dirname "$dest")" 2>/dev/null
if curl -fsSL --max-time 15 "$base/$path" -o "$tmp" 2>/dev/null && [ -s "$tmp" ]; then
if [ ! -f "$dest" ] || ! cmp -s "$dest" "$tmp"; then
mv "$tmp" "$dest"
case "$path" in *.sh) chmod +x "$dest" 2>/dev/null || true ;; esac
updated=$((updated + 1))
else
rm -f "$tmp"
fi
else
rm -f "$tmp"
failed=$((failed + 1))
fi
done < "$manifest"
rm -f "$manifest"
if [ "$updated" -gt 0 ] || [ "$failed" -gt 0 ]; then
log "manifest sync: $updated updated, $failed failed, $count total (from $base)"
fi
return 0
}
self_update() {
[ "$LARRY_NO_UPDATE" = "1" ] && return 0
[ -z "$LARRY_BASE_URL" ] && return 0
local self="$0"
case "$self" in /*) ;; *) self="$PWD/$self" ;; esac
# Phase A: local file sync. Triggered when on-disk files are out of sync
# with the running larry.sh version (e.g. just after a self-replace, or
# on first launch after install).
local last_sync=""
[ -f "$LARRY_HOME/.last-sync-version" ] \
&& last_sync=$(tr -d '[:space:]' < "$LARRY_HOME/.last-sync-version" 2>/dev/null)
if [ "$last_sync" != "$LARRY_VERSION" ]; then
if sync_from_manifest "$LARRY_BASE_URL"; then
printf '%s\n' "$LARRY_VERSION" > "$LARRY_HOME/.last-sync-version" 2>/dev/null || true
fi
fi
# Phase B: skip the network version check on the relaunch right after a
# self-replace (we just pulled it; checking again is pointless and risks
# loops if curl returns stale/partial content).
[ "${LARRY_JUST_UPDATED:-0}" = "1" ] && return 0
[ -w "$self" ] || return 0
local tmp="$LARRY_HOME/larry.sh.new"
if curl -fsSL --max-time 5 "$LARRY_UPDATE_URL" -o "$tmp" 2>/dev/null; then
if [ -s "$tmp" ] && ! cmp -s "$self" "$tmp"; then
local new_ver
new_ver=$(grep -m1 '^LARRY_VERSION=' "$tmp" | sed 's/.*"\(.*\)".*/\1/')
log "update found: $LARRY_VERSION -> ${new_ver:-?}"
cp "$tmp" "$self" && chmod +x "$self"
rm -f "$tmp"
log "relaunching..."
exec "$self" --no-update ${ARG_DIR:+"$ARG_DIR"}
fi
rm -f "$tmp"
fi
local remote_ver
remote_ver=$(curl -fsSL --max-time 5 "$LARRY_BASE_URL/VERSION" 2>/dev/null | tr -d '[:space:]')
[ -z "$remote_ver" ] && return 0
[ "$remote_ver" = "$LARRY_VERSION" ] && return 0
# Also refresh agents
if [ -n "$LARRY_AGENTS_URL" ]; then
for f in larry.md clover.md; do
curl -fsSL --max-time 5 "$LARRY_AGENTS_URL/$f" -o "$LARRY_HOME/agents/$f.new" 2>/dev/null \
&& [ -s "$LARRY_HOME/agents/$f.new" ] \
&& mv "$LARRY_HOME/agents/$f.new" "$LARRY_HOME/agents/$f" \
|| rm -f "$LARRY_HOME/agents/$f.new"
done
local tmp="$LARRY_HOME/larry.sh.new"
curl -fsSL --max-time 15 "$LARRY_BASE_URL/larry.sh" -o "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
[ -s "$tmp" ] || { rm -f "$tmp"; return 0; }
if cmp -s "$self" "$tmp"; then
rm -f "$tmp"
return 0
fi
local new_ver
new_ver=$(grep -m1 '^LARRY_VERSION=' "$tmp" | sed 's/.*"\(.*\)".*/\1/')
[ -z "$new_ver" ] && { rm -f "$tmp"; return 0; }
log "update found: $LARRY_VERSION -> $new_ver — relaunching"
cp "$tmp" "$self" && chmod +x "$self"
rm -f "$tmp"
# Force phase A on the next launch by invalidating the sync stamp.
rm -f "$LARRY_HOME/.last-sync-version" 2>/dev/null || true
exec env LARRY_JUST_UPDATED=1 "$self" ${ARG_DIR:+"$ARG_DIR"}
}
self_update

View File

@ -87,17 +87,30 @@ Claude Code uses). No API key needed.
${url}
2. Sign in with your Claude account.
3. Approve the app. You'll land on a page that displays the authorization
code in the URL (it'll look like https://console.anthropic.com/oauth/code/
callback?code=<LONG-STRING>&state=...). Copy ONLY the code value (the
part between code= and the next &).
3. Approve the app. You'll land on a page that displays a string in the form
<CODE>#<STATE>
(Anthropic uses a URL fragment, not a query param, to deliver them.)
Copy the WHOLE string — both halves and the '#' between them.
4. Paste it here:
EOF
printf 'authorization code: '
read -r code
[ -z "$code" ] && die "no code entered"
printf 'authorization code (CODE#STATE): '
read -r code_input
[ -z "$code_input" ] && die "no code entered"
# Split CODE#STATE. If the user pasted only the code (no '#'), keep the
# state we generated; otherwise verify the returned state matches.
local code returned_state
if [[ "$code_input" == *"#"* ]]; then
code="${code_input%%#*}"
returned_state="${code_input#*#}"
if [ -n "$returned_state" ] && [ "$returned_state" != "$state" ]; then
die "state mismatch — got '$returned_state', expected '$state' (possible CSRF or stale URL; rerun login)"
fi
else
code="$code_input"
fi
local resp
resp=$(curl -sS -X POST "$TOKEN_URL" \
@ -117,10 +130,15 @@ EOF
cat >&2 <<EOF
Hints:
- Make sure you pasted ONLY the code= value, not the whole URL.
- The code is single-use; if you used it already, run 'larry-auth.sh login' again.
- If the OAuth endpoint has changed, you can fall back to the API key
by deleting any oauth file and creating $LARRY_HOME/.env with
- Anthropic's callback delivers the code as CODE#STATE (fragment, not query).
Paste the WHOLE string including '#'. Just CODE alone will also work, but
if you pasted CODE#STATE#... or trimmed wrong, the token endpoint will
return 'rate_limit_error' (misleading — it actually means malformed/used
code, not a real rate limit).
- The code is single-use; if you used it already (even on a failed attempt),
run 'larry-auth.sh login' again to get a fresh URL.
- If the OAuth endpoint has genuinely changed, you can fall back to the API
key by deleting any oauth file and creating $LARRY_HOME/.env with
ANTHROPIC_API_KEY=sk-ant-...
EOF
exit 1