cloverleaf-larry/lib/broker.sh
bj 2b578f5058 v0.9.1: on upgrade to broker-mode, WIPE the now-obsolete local credentials
An install switching TO broker-mode (the v0.9.0 default) carried long-lived
Anthropic/OAuth credentials from the pre-broker era. Broker-mode authenticates
via short-lived broker tokens and never uses them — they 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 .oauth.json (reuses the
  uninstall-larry.sh shred -u -z -n3 -> overwrite -> rm logic).
- Strips the ANTHROPIC_API_KEY / CLAUDE_CODE_OAUTH_TOKEN LINES from
  $LARRY_HOME/.env and from ~/.bashrc, ~/.bash_profile, ~/.profile (backup
  first); every other line is kept.
- Idempotent (.broker-cred-wiped marker, written only after a run that removed
  something); silent no-op when clean.
- Hard-guarded on LARRY_AUTH_MODE=broker: does NOT fire under the apikey escape
  hatch (which legitimately still needs the key). Only the two Anthropic/OAuth
  vars are touched (LARRY_* / GITEA_TOKEN are still needed in broker mode).
- Prints a reminder to ALSO revoke at the source (local deletion != server
  revocation), per the decommission / kill-switch docs.

Fires 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.
VERSION + MANIFEST regenerated. Tested: 31/31 assertions pass across the
upgrade-wipe, apikey-non-wipe, clean-no-op, idempotency, dangerous-path-guard,
and selective-line-strip paths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-31 23:42:11 -07:00

480 lines
24 KiB
Bash

#!/usr/bin/env bash
# broker.sh — larry-broker client (the remote kill-switch client integration,
# Phase 3). Defines functions only; runs no code on source.
#
# WHY THIS EXISTS (Bryan, 2026-05-31): a deployed Cloverleaf-Larry on a client
# box (e.g. the Gundersen/Epic install) must NOT hold a long-lived sk-ant-… key
# Bryan would have to chase across the Anthropic console to kill. Instead the
# client holds a per-deployment ENROLLMENT SECRET, exchanges it for a SHORT-LIVED
# token from a broker Bryan controls (on .135), and routes every LLM call THROUGH
# the broker (/v1/messages) — the broker injects the real key server-side. Stop
# authorizing a deployment in the broker and it 401s and dies, with NO access to
# the box required. This is the DEFAULT rail for every Cloverleaf-Larry.
#
# Server contract (Mack's Phase-1 broker; /mnt/nas/docker/larry-broker):
# POST /enroll-mint {"deployment_id","enrollment_secret"}
# -> 200 {"token","expires_in","expires_at"} (authorized)
# -> 401 (unknown/bad/revoked)
# POST /v1/messages Authorization: Bearer <token> (Anthropic Messages shape)
# -> proxied to Anthropic; 401 the INSTANT the deployment is revoked.
# GET /authorized?dep=<id> (no auth) -> {authorized, profile, wipe_on_disable}
#
# FAIL-CLOSED is the whole point: if the client cannot CONFIRM authorization
# (disabled, OR N consecutive heartbeat misses), it REFUSES to run. For a
# profile:phi deployment it then runs a best-effort local PHI wipe (same
# secure-delete logic as uninstall-larry.sh) before exiting. "Can't confirm" is
# treated as "not authorized" — never as "assume OK and keep running".
#
# REACHABILITY (the critical design tension, flagged for Bryan): broker-mode
# means the client MUST reach the broker to function. The broker is LAN +
# Tailscale only (no public route). On an egress-restricted box (the Gundersen
# Cloudflare block that 28'd git.bjnoela.com), the client reaches the broker over
# TAILSCALE (LARRY_BROKER_URL=http://100.86.16.114:8181). If neither LAN nor
# Tailscale can reach the broker, broker-mode fail-closes = the agent will not
# run. That is a correct KILL state but a useless WORKING state, so a deployment
# on a locked-down network MUST have Tailscale (or a future hardened public
# broker ingress). See the README "Reachability" section.
#
# SOURCING NOTE: pure function defs; no set -e/-u/-o pipefail changes (the caller
# owns those). Listed in MANIFEST so it propagates + stays auditable.
# ── Config (caller sets these before sourcing; sane defaults here) ───────────
# LARRY_BROKER_URL broker base URL (LAN: http://192.168.20.135:8181 ;
# Tailscale: http://100.86.16.114:8181). Default = tailnet
# so an off-LAN client works out of the box.
# LARRY_DEPLOYMENT_ID this deployment's id in the broker registry.
# LARRY_ENROLL_SECRET the per-deployment enrollment secret (or in
# $LARRY_HOME/.enroll-secret, mode 0600).
# LARRY_PROFILE "default" | "phi". phi => wipe local PHI on disable.
# LARRY_HEARTBEAT_INTERVAL seconds between /authorized polls (default 60).
# LARRY_HEARTBEAT_MAX_MISS consecutive misses tolerated before fail-close
# (default 3). misses are unreachable broker; an
# explicit authorized:false fails closed IMMEDIATELY.
LARRY_BROKER_URL="${LARRY_BROKER_URL:-http://100.86.16.114:8181}"
LARRY_DEPLOYMENT_ID="${LARRY_DEPLOYMENT_ID:-}"
LARRY_ENROLL_SECRET="${LARRY_ENROLL_SECRET:-}"
LARRY_PROFILE="${LARRY_PROFILE:-default}"
LARRY_HEARTBEAT_INTERVAL="${LARRY_HEARTBEAT_INTERVAL:-60}"
LARRY_HEARTBEAT_MAX_MISS="${LARRY_HEARTBEAT_MAX_MISS:-3}"
# Runtime state (in-memory; the token never touches disk).
_BROKER_TOKEN=""
_BROKER_TOKEN_EXP=0 # unix epoch the token expires at
_BROKER_MISS_COUNT=0 # consecutive heartbeat misses
_BROKER_LAST_PROFILE="" # profile the broker last reported (authoritative)
# _broker_strip_cr — defensive CR-strip (MobaXterm/Cygwin paste taints).
_broker_strip_cr() { local v="${1:-}"; printf '%s' "${v//$'\r'/}"; }
# _broker_load_secret — resolve the enrollment secret from env or the 0600 file.
_broker_load_secret() {
if [ -n "$LARRY_ENROLL_SECRET" ]; then
LARRY_ENROLL_SECRET="$(_broker_strip_cr "$LARRY_ENROLL_SECRET")"
return 0
fi
local f="${LARRY_HOME:-$HOME/.larry}/.enroll-secret"
if [ -f "$f" ]; then
LARRY_ENROLL_SECRET="$(_broker_strip_cr "$(cat "$f" 2>/dev/null)")"
LARRY_ENROLL_SECRET="${LARRY_ENROLL_SECRET//$'\n'/}"
fi
[ -n "$LARRY_ENROLL_SECRET" ]
}
# _broker_json_field BODY KEY — extract a top-level JSON string/number/bool value
# WITHOUT requiring jq (the locked-down boxes may not have it). Prefers jq when
# present; falls back to a tolerant sed/grep. Returns the value on stdout.
_broker_json_field() {
local body="$1" key="$2"
if command -v jq >/dev/null 2>&1; then
# NB: do NOT use `// empty` — in jq the alternative operator treats a literal
# `false` (and `null`) as empty, so `"authorized": false` would parse as ""
# and the heartbeat would mis-classify a DISABLED deployment as an unreachable
# MISS (delaying fail-close past the miss budget and skipping the PHI wipe).
# Map an absent key to empty explicitly; render false/null/numbers verbatim.
local _jq; _jq="$(printf '%s' "$body" | jq -r --arg k "$key" 'if has($k) then .[$k] else "" end' 2>/dev/null)"
if [ -n "$_jq" ] || printf '%s' "$body" | jq -e --arg k "$key" 'has($k)' >/dev/null 2>&1; then
printf '%s' "$_jq"; return 0
fi
fi
# Fallback: match "key": "value" OR "key": value (bool/number/null).
printf '%s' "$body" \
| tr -d '\r\n' \
| grep -oE "\"$key\"[[:space:]]*:[[:space:]]*(\"[^\"]*\"|true|false|null|[0-9]+)" \
| head -1 \
| sed -E "s/.*:[[:space:]]*//; s/^\"//; s/\"$//"
}
# ── Enroll + mint ────────────────────────────────────────────────────────────
# _broker_enroll_mint — exchange (deployment_id, enroll_secret) for a short-lived
# token. Sets _BROKER_TOKEN / _BROKER_TOKEN_EXP on success. The secret is fed via
# curl --data @- on stdin (off argv / the process table). Returns:
# 0 = minted (authorized)
# 3 = unauthorized (401: unknown id / bad secret / REVOKED — fail closed)
# 4 = unreachable (curl/network/DNS — fail closed after the miss budget)
_broker_enroll_mint() {
command -v curl >/dev/null 2>&1 || return 4
_broker_load_secret || { _broker_log_err "no enrollment secret (set LARRY_ENROLL_SECRET or $LARRY_HOME/.enroll-secret)"; return 3; }
[ -n "$LARRY_DEPLOYMENT_ID" ] || { _broker_log_err "LARRY_DEPLOYMENT_ID is unset — cannot enroll"; return 3; }
local url="$LARRY_BROKER_URL/enroll-mint"
local body code resp tmp
# Build the request body on a tmpfile (secret never on argv).
tmp="$(mktemp 2>/dev/null || echo "")"
if [ -n "$tmp" ]; then
printf '{"deployment_id":"%s","enrollment_secret":"%s"}' \
"$LARRY_DEPLOYMENT_ID" "$LARRY_ENROLL_SECRET" > "$tmp"
resp="$(curl -sS --max-time 20 -w $'\n%{http_code}' \
-H 'content-type: application/json' \
--data-binary "@$tmp" "$url" 2>/dev/null)"; code=$?
rm -f "$tmp"
else
resp="$(curl -sS --max-time 20 -w $'\n%{http_code}' \
-H 'content-type: application/json' \
--data-binary "{\"deployment_id\":\"$LARRY_DEPLOYMENT_ID\",\"enrollment_secret\":\"$LARRY_ENROLL_SECRET\"}" \
"$url" 2>/dev/null)"; code=$?
fi
[ "$code" != "0" ] && { _broker_log_err "broker unreachable at $url (curl rc=$code)"; return 4; }
local http="${resp##*$'\n'}" payload="${resp%$'\n'*}"
if [ "$http" = "200" ]; then
local tok exp
tok="$(_broker_json_field "$payload" token)"
exp="$(_broker_json_field "$payload" expires_at)"
if [ -n "$tok" ]; then
_BROKER_TOKEN="$tok"
# expires_at is absolute epoch; if absent derive from expires_in.
if [ -n "$exp" ]; then
_BROKER_TOKEN_EXP="$exp"
else
local ein; ein="$(_broker_json_field "$payload" expires_in)"
_BROKER_TOKEN_EXP=$(( $(date +%s 2>/dev/null || echo 0) + ${ein:-60} ))
fi
_BROKER_MISS_COUNT=0
return 0
fi
_broker_log_err "broker 200 but no token in response"
return 4
fi
if [ "$http" = "401" ]; then
return 3 # unknown / bad secret / REVOKED — fail closed, do not retry-loop
fi
_broker_log_err "broker enroll-mint HTTP $http (treating as unreachable)"
return 4
}
# _broker_token_valid — true if we hold a token with >20s of life left.
_broker_token_valid() {
[ -n "$_BROKER_TOKEN" ] || return 1
local now; now=$(date +%s 2>/dev/null || echo 0)
[ "$_BROKER_TOKEN_EXP" -gt "$(( now + 20 ))" ] 2>/dev/null
}
# _broker_ensure_token — return a live token on stdout, minting/refreshing if the
# current one is missing or near-expiry. Returns the mint rc (0/3/4) so the
# caller can fail closed on 3/4.
_broker_ensure_token() {
if _broker_token_valid; then printf '%s' "$_BROKER_TOKEN"; return 0; fi
_broker_enroll_mint; local rc=$?
[ "$rc" = "0" ] && printf '%s' "$_BROKER_TOKEN"
return $rc
}
# ── Heartbeat (fail-closed authorization check) ──────────────────────────────
# _broker_heartbeat — GET /authorized?dep=<id>. Updates _BROKER_LAST_PROFILE.
# Returns:
# 0 = authorized:true (run)
# 3 = authorized:false (DISABLED — fail closed immediately)
# 4 = unreachable / unparsable (a MISS — caller increments the miss budget)
_broker_heartbeat() {
command -v curl >/dev/null 2>&1 || return 4
[ -n "$LARRY_DEPLOYMENT_ID" ] || return 3
local url="$LARRY_BROKER_URL/authorized?dep=$LARRY_DEPLOYMENT_ID"
local resp code http payload
resp="$(curl -sS --max-time 12 -w $'\n%{http_code}' "$url" 2>/dev/null)"; code=$?
[ "$code" != "0" ] && return 4
http="${resp##*$'\n'}"; payload="${resp%$'\n'*}"
[ "$http" = "200" ] || return 4
local authd prof
authd="$(_broker_json_field "$payload" authorized)"
prof="$(_broker_json_field "$payload" profile)"
[ -n "$prof" ] && [ "$prof" != "null" ] && _BROKER_LAST_PROFILE="$prof"
case "$authd" in
true) _BROKER_MISS_COUNT=0; return 0 ;;
false) return 3 ;;
*) return 4 ;; # could not parse authorized => treat as a miss
esac
}
# _broker_preflight_gate — the launch-time fail-closed gate. Polls /authorized
# once (with a tiny retry to tolerate a transient blip within the miss budget).
# On confirmed authorized:true => 0. On authorized:false => triggers PHI wipe (if
# phi) and returns 3. On unreachable past the budget => returns 4 (caller blocks;
# NO wipe on unreachable — we can't confirm a revoke, only that we can't reach
# home, so wiping on every network blip would be destructive). Bryan's rule:
# fail-closed = does not RUN; PHI wipe fires only on an explicit disable.
_broker_preflight_gate() {
local tries=0 max="$LARRY_HEARTBEAT_MAX_MISS" rc
while [ "$tries" -lt "$max" ]; do
_broker_heartbeat; rc=$?
case "$rc" in
0) return 0 ;; # authorized — run
3) _broker_on_disabled; return 3 ;; # explicit revoke — wipe(phi)+block
4) tries=$(( tries + 1 )); [ "$tries" -lt "$max" ] && sleep 2 ;;
esac
done
return 4 # unreachable past the budget — fail closed (block), no wipe
}
# _broker_on_disabled — invoked when the broker says authorized:false. For a
# phi profile, runs the best-effort local PHI wipe, then the caller blocks/exits.
_broker_on_disabled() {
_broker_log_err "deployment '$LARRY_DEPLOYMENT_ID' is DISABLED in the broker — refusing to run."
local prof="${_BROKER_LAST_PROFILE:-$LARRY_PROFILE}"
if [ "$prof" = "phi" ]; then
_broker_log_err "profile=phi — running best-effort local PHI wipe (see uninstall-larry.sh for the guaranteed path)."
_broker_phi_wipe
fi
}
# ── Best-effort PHI wipe (reuses uninstall-larry.sh's secure-delete logic) ───
# _broker_secure_delete FILE — shred -u -z -n3 if available; else overwrite then
# rm (best-effort on Windows/CoW/SSD); else plain rm. Echoes the method achieved.
# BYTE-FOR-BYTE the same approach as uninstall-larry.sh secure_delete().
_broker_secure_delete() {
local f="$1"
[ -f "$f" ] || { echo "absent"; return 0; }
if command -v shred >/dev/null 2>&1; then
if shred -u -z -n 3 "$f" 2>/dev/null; then echo "shred"; return 0; fi
if shred -u "$f" 2>/dev/null; then echo "shred"; return 0; fi
fi
local sz; sz="$(wc -c < "$f" 2>/dev/null || echo 0)"
if [ "${sz:-0}" -gt 0 ] 2>/dev/null; then
if command -v dd >/dev/null 2>&1; then
dd if=/dev/zero of="$f" bs=1 count="$sz" conv=notrunc 2>/dev/null || true
[ -r /dev/urandom ] && dd if=/dev/urandom of="$f" bs=1 count="$sz" conv=notrunc 2>/dev/null || true
else
: > "$f" 2>/dev/null || true
fi
fi
rm -f "$f" 2>/dev/null && echo "overwrite" || echo "FAILED"
}
# _broker_phi_wipe — securely delete the known finite list of cleartext-PHI
# artifacts under $LARRY_HOME. Same target list as uninstall-larry.sh collect_phi()
# PLUS the per-client credentials (.api-key/.env/.enroll-secret) and broker token
# state, since a disabled deployment should leave nothing usable behind. This is
# BEST-EFFORT (Finding 4 of the design brief): it only fires if the agent runs
# again while online and the script is intact; it cannot touch a powered-off box,
# and deletion is not guaranteed-unrecoverable on SSD/CoW/Windows. The guaranteed
# path remains the machine owner running uninstall-larry.sh.
_broker_phi_wipe() {
local lh="${LARRY_HOME:-$HOME/.larry}"
# HARD SAFETY GUARD (mirrors uninstall-larry.sh): never operate on an empty /
# root / $HOME path.
local norm; norm="$(printf '%s' "$lh" | sed 's:/*$::')"
case "$norm" in
""|"/"|"/root"|"/home"|"/Users"|"/usr"|"/etc"|"/var"|"/bin"|"/tmp"|"/.larry")
_broker_log_err "PHI wipe refused: LARRY_HOME ('$lh') resolves to a dangerous path."; return 1 ;;
esac
case "$norm" in */.larry|*larry*) : ;; *)
_broker_log_err "PHI wipe refused: LARRY_HOME ('$lh') doesn't look like a Larry install dir."; return 1 ;;
esac
[ -n "${HOME:-}" ] && [ "$norm" = "$(printf '%s' "$HOME" | sed 's:/*$::')" ] && {
_broker_log_err "PHI wipe refused: LARRY_HOME equals \$HOME."; return 1; }
local targets=() f
# Cleartext-PHI artifacts (per design brief Finding 6 + decommission §7.3).
for f in "$lh/log/auto-phi.log" "$lh/sanitize/lookup.tsv"; do
[ -f "$f" ] && targets+=("$f")
done
# Session transcripts (may contain PHI).
if [ -d "$lh/sessions" ]; then
if command -v find >/dev/null 2>&1; then
while IFS= read -r f; do [ -n "$f" ] && targets+=("$f"); done \
< <(find "$lh/sessions" -type f -name '*.log.md' 2>/dev/null)
else
local _ng; _ng="$(shopt -p nullglob 2>/dev/null || true)"; shopt -s nullglob 2>/dev/null || true
for f in "$lh"/sessions/*.log.md "$lh"/sessions/**/*.log.md; do [ -f "$f" ] && targets+=("$f"); done
eval "$_ng" 2>/dev/null || true
fi
fi
# Credentials — a disabled deployment must not leave usable secrets. (In broker
# mode there is no .api-key by design, but a prior apikey-mode install may have
# left one; the enroll secret + any legacy key go too.)
for f in "$lh/.enroll-secret" "$lh/.api-key" "$lh/.env" "$lh/.oauth.json"; do
[ -f "$f" ] && targets+=("$f")
done
local shredded=0 best=0 failed=0 method
for f in "${targets[@]}"; do
method="$(_broker_secure_delete "$f")"
case "$method" in
shred) shredded=$(( shredded + 1 )) ;;
overwrite) best=$(( best + 1 )) ;;
absent) : ;;
*) failed=$(( failed + 1 )) ;;
esac
done
# Scrub the in-memory token too.
_BROKER_TOKEN=""; _BROKER_TOKEN_EXP=0
if command -v shred >/dev/null 2>&1; then
_broker_log_err "PHI wipe: $shredded shredded, $best best-effort, $failed failed."
else
_broker_log_err "PHI wipe (no 'shred' on this platform): $best removed best-effort, $failed failed."
_broker_log_err " Treat the disk as possibly still holding PHI remnants; the guaranteed delete is the machine owner running uninstall-larry.sh."
fi
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() {
if command -v err >/dev/null 2>&1; then err "broker: $*"; else printf 'broker: %s\n' "$*" >&2; fi
}