From 28622ca40b6c5fc5610ce302ff24fd919a0f5bab Mon Sep 17 00:00:00 2001 From: Bryan Johnson Date: Wed, 27 May 2026 08:50:46 -0700 Subject: [PATCH] v0.5.0: MANIFEST-driven self-update + OAuth code#state parsing MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- MANIFEST | 62 ++++++++++++++++++++++ VERSION | 2 +- larry.sh | 141 ++++++++++++++++++++++++++++++++++++++++----------- lib/oauth.sh | 40 +++++++++++---- 4 files changed, 203 insertions(+), 42 deletions(-) create mode 100644 MANIFEST diff --git a/MANIFEST b/MANIFEST new file mode 100644 index 0000000..85681f7 --- /dev/null +++ b/MANIFEST @@ -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 diff --git a/VERSION b/VERSION index 17b2ccd..8f0916f 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.4.3 +0.5.0 diff --git a/larry.sh b/larry.sh index 2df2fd4..eacb76f 100755 --- a/larry.sh +++ b/larry.sh @@ -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 diff --git a/lib/oauth.sh b/lib/oauth.sh index e258b47..52b2c61 100755 --- a/lib/oauth.sh +++ b/lib/oauth.sh @@ -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=&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 + # + (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 <