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>
480 lines
24 KiB
Bash
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
|
|
}
|