diff --git a/CHANGELOG.md b/CHANGELOG.md index f058d8e..c2e684f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,42 @@ All notable changes to `cloverleaf-larry` / `larry-anywhere` are recorded here. Versioning is loose-semver; bumps trigger the in-process self-update on every running client via `LARRY_BASE_URL` + `MANIFEST`. +## v0.9.1 — 2026-05-31 + +**Upgrade to broker-mode now WIPES the now-obsolete local Anthropic/OAuth +credentials it carried from the pre-broker era.** + +When an existing install self-updates and `LARRY_AUTH_MODE` resolves to `broker` +(the v0.9.0 default), the long-lived credentials baked in before the broker +pivot are unused by broker-mode (which authenticates via short-lived broker +tokens) and are a pure security liability on the box — acutely so on a PHI box. +On the next self-update the agent now cleans them up automatically: + +- **Secure-deletes** `$LARRY_HOME/.api-key` and `$LARRY_HOME/.oauth.json` + (reuses the `uninstall-larry.sh` `shred -u -z -n3` → overwrite → rm logic via + `lib/broker.sh` `_broker_secure_delete`, since on a PHI box these are sensitive). +- **Strips the credential LINES** (`ANTHROPIC_API_KEY` / `CLAUDE_CODE_OAUTH_TOKEN`, + with optional `export`/leading whitespace) from `$LARRY_HOME/.env` and from + `~/.bashrc`, `~/.bash_profile`, `~/.profile` — keeping every other line; a + timestamped `.broker-upgrade..bak` is written before any rc/.env rewrite. +- Logs clearly what was wiped, file by file, with the secure-delete method used. +- **Idempotent:** writes `$LARRY_HOME/.broker-cred-wiped` only after a run that + actually removed something, so the rc-rewrite path isn't re-attempted every + launch; if nothing obsolete is present it no-ops silently and leaves no marker + (so a later `apikey`→`broker` flip still triggers cleanup). +- **Does NOT fire when `LARRY_AUTH_MODE=apikey`** — the escape hatch legitimately + still needs its key. Hard-guarded on `LARRY_AUTH_MODE=broker` inside the + function; only the two Anthropic/OAuth vars are touched (broker-mode still uses + `LARRY_*` and `GITEA_TOKEN`, so those rc lines are left intact). +- Prints a reminder that the operator must ALSO revoke the keys at the source + (local deletion ≠ server revocation), consistent with the decommission / + kill-switch docs. + +Fires in `larry.sh` at the broker-resolution block (after `self_update` synced a +fresh `lib/broker.sh`, before the fail-closed preflight). New functions in +`lib/broker.sh`: `_broker_wipe_obsolete_credentials`, +`_broker_strip_cred_lines_from_env`, `_broker_strip_cred_lines_from_rc`. + ## v0.9.0 — 2026-05-31 **Broker mode is the DEFAULT — the remote kill-switch is wired into every diff --git a/MANIFEST b/MANIFEST index 85f5346..020180b 100644 --- a/MANIFEST +++ b/MANIFEST @@ -23,7 +23,7 @@ # scripts/make-manifest.sh and bump VERSION. # Top-level scripts -larry.sh 55c6fb42f09c923e6a8f5a8dfa9dbdd8fb5a6012dd5ac6707f87e853abcec234 +larry.sh 25d81d239b268635bff6704b55f473dc795af844b7a77cf6b24ba7e214c25786 larry-tunnel.sh 6b050e4eeab15669f4858eaf3b807f168f211ced07815db9521bc40a093f6aaa larry-auth.sh a220cdf7878569dc3028951ee57fc8d5e706a8ca5c6aa45347b58facb386f831 larry-rollback.sh 91b5e9aa6c79266bf306dcfba4ca791c07971bd6924d67a779037531648aa6d0 @@ -31,9 +31,9 @@ install-larry.sh 072a036ad5bbf80e866cfd2dd74de50f8defd69a3f835032579b0cb9d421ad5 uninstall-larry.sh c53ad2d8354c7adeb243b541f027f3f481e4a8661eecfd7af14d7ca53cfcaad9 # Metadata -VERSION 9d8c94f1ad3ea96b1e2ac4914fda4cb93c76b4a3e0d8cc6dd8976d6c0b227d15 +VERSION 179a5390966e85c2071a87c1b31de13df67460665196f6529f8c4986842f81e5 MANUAL.md 5ff54d6d5fae826f8b3da1eb3be6476076bb15f9b1417a4de285e59ea37e1b1f -CHANGELOG.md b3c48567452b4302b1167a3a9fb11f02964312a2f03ec393f8d35c9a1d392d90 +CHANGELOG.md 934007dc1b08b6c90120f009e3cc7870815e7b251fdf8f6629aa4c004c866017 # Agent personas (system-prompt overlays) agents/larry.md 0a1ef737e7fc133ab35be09f79c3a4df33de814e0404b69b950932d0c8a01be1 @@ -55,7 +55,7 @@ lib/oauth.sh 04a93376f88fe53cc1c86a5dbe577735c60375dadd4f2fda55b921ef3cddf22b # v0.9.0: broker client — the remote kill-switch (default rail). Enroll+mint a # short-lived token, route LLM calls through the broker, fail-closed heartbeat, # best-effort PHI wipe on disable. -lib/broker.sh fe05f7b349d68239b3683b1f8c3139358f586cb583c907638483f1d3389959c4 +lib/broker.sh 8ec200439329ca0f93003d0976589f427de29450bbc8afab0fe32838e862bdce # Secure SSH with ControlMaster (password hidden from Larry-the-LLM) lib/ssh-helper.sh 18df1f1f1936c930ba0197c0e0b4bd89c027500de99b56067b620ca9144f6e9e diff --git a/VERSION b/VERSION index ac39a10..f374f66 100644 --- a/VERSION +++ b/VERSION @@ -1 +1 @@ -0.9.0 +0.9.1 diff --git a/larry.sh b/larry.sh index 42651b9..7a97d25 100755 --- a/larry.sh +++ b/larry.sh @@ -99,7 +99,7 @@ set -o pipefail # ───────────────────────────────────────────────────────────────────────────── # Config # ───────────────────────────────────────────────────────────────────────────── -LARRY_VERSION="0.9.0" +LARRY_VERSION="0.9.1" LARRY_HOME="${LARRY_HOME:-$HOME/.larry}" # ───────────────────────────────────────────────────────────────────────────── @@ -1733,6 +1733,17 @@ if [ "$LARRY_AUTH_MODE" = "broker" ] && [ "$LARRY_NO_API" != "1" ]; then exit 1 fi + # UPGRADE-TO-BROKER CREDENTIAL CLEANUP (v0.9.1): an install switching TO broker + # mode carried long-lived Anthropic/OAuth credentials from the pre-broker era. + # Broker mode never uses them (it mints short-lived broker tokens) — they're a + # pure security liability on the box, acutely so on a PHI box. Secure-delete + # .api-key/.oauth.json and strip the key/oauth LINES from .env + the shell rc + # files, keeping everything else. Idempotent (no-ops once clean); fires ONLY in + # broker mode (the apikey escape hatch legitimately still needs its key, so this + # is guarded inside the function on LARRY_AUTH_MODE=broker). Prints a reminder to + # ALSO revoke at the source. See lib/broker.sh _broker_wipe_obsolete_credentials. + _broker_wipe_obsolete_credentials + # Resolve a deployment id: explicit env wins; else derive a stable per-box id so # "install larry on a box" auto-enrolls under a named deployment in the dashboard. if [ -z "$LARRY_DEPLOYMENT_ID" ]; then diff --git a/lib/broker.sh b/lib/broker.sh index 5e43335..a690c4f 100644 --- a/lib/broker.sh +++ b/lib/broker.sh @@ -327,6 +327,151 @@ _broker_phi_wipe() { return 0 } +# ── Upgrade-to-broker credential cleanup (v0.9.1) ──────────────────────────── +# When an existing install switches TO broker-mode, the long-lived Anthropic +# credentials it carried from the pre-broker (apikey/oauth) era are UNUSED by +# broker-mode (which authenticates via short-lived broker tokens) and are a pure +# security liability on the box — most acutely on a PHI box. This wipes them. +# +# WHAT IT WIPES (only when LARRY_AUTH_MODE resolved to "broker"): +# - $LARRY_HOME/.api-key — secure-deleted (it's a whole-file secret) +# - $LARRY_HOME/.oauth.json — secure-deleted (whole-file OAuth refresh token) +# - $LARRY_HOME/.env — the ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN +# LINES are stripped; the rest of .env is KEPT. +# - ~/.bashrc, ~/.bash_profile, ~/.profile — exported ANTHROPIC_API_KEY / +# CLAUDE_CODE_OAUTH_TOKEN lines stripped (backup +# first); other lines kept. +# +# IDEMPOTENT: a marker ($LARRY_HOME/.broker-cred-wiped) is written after a run +# that found something, so the (potentially destructive) rc-rewrite path is not +# re-attempted every launch. If NOTHING obsolete is present it no-ops silently +# and does NOT write the marker (so a later apikey→broker flip still cleans up). +# +# GUARD: the caller MUST only invoke this when LARRY_AUTH_MODE is "broker". In +# apikey mode the key is legitimately still needed — do NOT call this there. +# +# NOT a substitute for revocation: local deletion ≠ server revocation. It prints +# the same "revoke at the source" reminder as uninstall-larry.sh / the +# decommission checklist. + +# _broker_strip_cred_lines_from_env FILE — remove only the ANTHROPIC_API_KEY and +# CLAUDE_CODE_OAUTH_TOKEN assignment/export lines from a key=value .env, keeping +# every other line. Backs up first. Echoes "stripped" | "none" | "FAILED". +# Matches: ANTHROPIC_API_KEY=… , export ANTHROPIC_API_KEY=… , and the OAuth twin +# (with optional leading whitespace), never a substring of another var name. +_broker_strip_cred_lines_from_env() { + local f="$1" + [ -f "$f" ] || { echo "absent"; return 0; } + local re='^[[:space:]]*(export[[:space:]]+)?(ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)=' + grep -qE "$re" "$f" 2>/dev/null || { echo "none"; return 0; } + local ts; ts="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo bak)" + cp -p "$f" "$f.broker-upgrade.$ts.bak" 2>/dev/null \ + || { _broker_log_err "could not back up $f — leaving it UNCHANGED for safety"; echo "FAILED"; return 1; } + if grep -vE "$re" "$f" > "$f.broker-tmp" 2>/dev/null && mv "$f.broker-tmp" "$f" 2>/dev/null; then + echo "stripped"; return 0 + fi + rm -f "$f.broker-tmp" 2>/dev/null || true + _broker_log_err "could not rewrite $f — remove the credential line(s) by hand" + echo "FAILED"; return 1 +} + +# _broker_strip_cred_lines_from_rc FILE — same idea for a shell profile: strip +# only the exported ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN lines, keep the +# rest, backup first. (Mirrors uninstall-larry.sh's rc-strip, but scoped to the +# two Anthropic/OAuth vars only — broker-mode still legitimately uses LARRY_* and +# GITEA_TOKEN, so we must NOT strip those here.) Echoes stripped|none|FAILED. +_broker_strip_cred_lines_from_rc() { + local f="$1" + [ -f "$f" ] || { echo "absent"; return 0; } + local re='^[[:space:]]*(export[[:space:]]+)?(ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)=' + grep -qE "$re" "$f" 2>/dev/null || { echo "none"; return 0; } + local ts; ts="$(date +%Y%m%d-%H%M%S 2>/dev/null || echo bak)" + cp -p "$f" "$f.broker-upgrade.$ts.bak" 2>/dev/null \ + || { _broker_log_err "could not back up $f — leaving it UNCHANGED for safety"; echo "FAILED"; return 1; } + if grep -vE "$re" "$f" > "$f.broker-tmp" 2>/dev/null && mv "$f.broker-tmp" "$f" 2>/dev/null; then + echo "stripped"; return 0 + fi + rm -f "$f.broker-tmp" 2>/dev/null || true + _broker_log_err "could not rewrite $f — remove the credential line(s) by hand" + echo "FAILED"; return 1 +} + +# _broker_wipe_obsolete_credentials — the upgrade-to-broker cleanup entry point. +# Call ONLY when LARRY_AUTH_MODE == "broker". Secure-deletes the whole-file +# secrets and strips the key/oauth lines from .env + the shell rc files. No-ops +# (and prints nothing) when there is nothing obsolete to remove. +_broker_wipe_obsolete_credentials() { + [ "${LARRY_AUTH_MODE:-}" = "broker" ] || return 0 # hard guard: broker only + local lh="${LARRY_HOME:-$HOME/.larry}" + + # Same dangerous-path guard family as _broker_phi_wipe (don't operate on / etc). + local norm; norm="$(printf '%s' "$lh" | sed 's:/*$::')" + case "$norm" in + ""|"/"|"/root"|"/home"|"/Users"|"/usr"|"/etc"|"/var"|"/bin"|"/tmp") + _broker_log_err "credential cleanup skipped: LARRY_HOME ('$lh') resolves to a dangerous path."; return 1 ;; + esac + + # Detect whether any obsolete credential is actually present. If not, no-op and + # leave NO marker (so a later apikey→broker flip still triggers cleanup). + local rc_re='^[[:space:]]*(export[[:space:]]+)?(ANTHROPIC_API_KEY|CLAUDE_CODE_OAUTH_TOKEN)=' + local present=0 rc + [ -f "$lh/.api-key" ] && present=1 + [ -f "$lh/.oauth.json" ] && present=1 + [ -f "$lh/.env" ] && grep -qE "$rc_re" "$lh/.env" 2>/dev/null && present=1 + for rc in "${HOME:-}/.bashrc" "${HOME:-}/.bash_profile" "${HOME:-}/.profile"; do + [ -n "$rc" ] && [ -f "$rc" ] && grep -qE "$rc_re" "$rc" 2>/dev/null && { present=1; break; } + done + [ "$present" = "1" ] || return 0 + + _broker_log_err "── upgrade to broker-mode: wiping now-obsolete local Anthropic/OAuth credentials ──" + _broker_log_err " broker-mode authenticates via short-lived broker tokens; these baked credentials" + _broker_log_err " are unused and a security liability on this box. Removing them now." + + local m + # 1. Whole-file secrets — secure-delete (reuses uninstall-larry.sh shred logic). + if [ -f "$lh/.api-key" ]; then + m="$(_broker_secure_delete "$lh/.api-key")" + _broker_log_err " .api-key: $m" + fi + if [ -f "$lh/.oauth.json" ]; then + m="$(_broker_secure_delete "$lh/.oauth.json")" + _broker_log_err " .oauth.json: $m" + fi + # 2. .env — strip only the two cred LINES, keep the rest of the file. + if [ -f "$lh/.env" ]; then + m="$(_broker_strip_cred_lines_from_env "$lh/.env")" + case "$m" in + stripped) _broker_log_err " .env: stripped ANTHROPIC_API_KEY/CLAUDE_CODE_OAUTH_TOKEN line(s) (rest kept; .bak written)" ;; + none) : ;; + *) _broker_log_err " .env: $m" ;; + esac + fi + # 3. Shell profiles — strip exported key/oauth lines (backup first), keep rest. + for rc in "${HOME:-}/.bashrc" "${HOME:-}/.bash_profile" "${HOME:-}/.profile"; do + [ -n "$rc" ] || continue + m="$(_broker_strip_cred_lines_from_rc "$rc")" + case "$m" in + stripped) _broker_log_err " $rc: stripped exported key/oauth line(s) (rest kept; .bak written)" ;; + none|absent) : ;; + *) _broker_log_err " $rc: $m" ;; + esac + done + + # 4. Idempotency marker — written only because we found+acted on something. + : > "$lh/.broker-cred-wiped" 2>/dev/null || true + date -u +%Y-%m-%dT%H:%M:%SZ > "$lh/.broker-cred-wiped" 2>/dev/null || true + + # 5. Revocation reminder — local deletion ≠ server revocation (decommission §6, + # kill-switch design). The operator MUST also revoke at the source. + _broker_log_err "REMINDER: local deletion does NOT revoke these credentials at the source." + _broker_log_err " Also REVOKE them now so a copy that already egressed cannot be used:" + _broker_log_err " 1. Anthropic Console → Settings → API Keys: DELETE the key for this box." + _broker_log_err " 2. Anthropic Console → account security / Connected apps: REVOKE the" + _broker_log_err " Claude-Code OAuth grant ('sign out everywhere'). See the decommission checklist." + _broker_log_err "── credential cleanup complete ──" + return 0 +} + # _broker_log_err — route through larry's err() if present, else stderr. Never # logs a token or secret. _broker_log_err() {