#!/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 (Anthropic Messages shape) # -> proxied to Anthropic; 401 the INSTANT the deployment is revoked. # GET /authorized?dep= (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=. 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 }