#!/usr/bin/env bash # sync-credentials.sh — Single source of truth for all OpenClaw credentials # Reads from canonical sources → pushes to all agent auth-profiles.json # # Usage: # sync-credentials.sh # Sync all credentials # sync-credentials.sh --restart # Sync + restart Atomizer cluster # sync-credentials.sh --check # Just check expiry/health, no changes # sync-credentials.sh --codex-login # Run codex login first, then sync set -euo pipefail RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; NC='\033[0m' MODE="${1:-sync}" if [ "$MODE" = "--codex-login" ]; then echo "Starting codex login..." echo "⚠️ Make sure you have SSH tunnel: ssh -L 1455:localhost:1455 clawdbot" codex login MODE="--restart" # After login, sync and restart fi export SYNC_MODE="$MODE" python3 << 'PYEOF' import json, os, time, sys, glob, shutil from pathlib import Path mode = os.environ.get("SYNC_MODE", "sync") home = os.path.expanduser("~") now_ms = int(time.time() * 1000) now_s = time.time() warnings = [] updates = [] # ─── Canonical credential sources ─── # 1. Anthropic token (from Mario's main profile — the "source of truth") mario_auth = f"{home}/.openclaw/agents/main/agent/auth-profiles.json" with open(mario_auth) as f: mario_profiles = json.load(f)["profiles"] anthropic_profile = mario_profiles.get("anthropic:default") google_profile = mario_profiles.get("google:default") # 2. OpenAI Codex (from Codex CLI — always freshest) codex_auth_path = f"{home}/.codex/auth.json" codex_profile = None if os.path.isfile(codex_auth_path): with open(codex_auth_path) as f: codex = json.load(f) t = codex["tokens"] # Estimate expiry from access token (JWT) import base64 try: payload = t["access_token"].split(".")[1] payload += "=" * (-len(payload) % 4) jwt = json.loads(base64.urlsafe_b64decode(payload)) expires_ms = jwt["exp"] * 1000 except: expires_ms = now_ms + 10 * 24 * 3600 * 1000 # fallback: 10 days codex_profile = { "type": "oauth", "provider": "openai-codex", "access": t["access_token"], "refresh": t["refresh_token"], "expires": expires_ms, "accountId": t.get("account_id", "") } days_left = (expires_ms - now_ms) / (24 * 3600 * 1000) if days_left < 2: warnings.append(f"⚠️ OpenAI Codex token expires in {days_left:.1f} days! Run: codex login") elif days_left < 5: warnings.append(f"⚡ OpenAI Codex token expires in {days_left:.1f} days") else: print(f" ✓ OpenAI Codex token valid for {days_left:.1f} days") else: warnings.append("⚠️ No Codex CLI auth found! Run: codex login") # ─── Check mode: just report ─── if mode == "--check": # Check Anthropic if anthropic_profile: print(f" ✓ Anthropic token: present (token type, no expiry)") # Check Google if google_profile: print(f" ✓ Google AI token: present") # Check Discord tokens discord_env = f"{home}/atomizer/config/.discord-tokens.env" if os.path.isfile(discord_env): with open(discord_env) as f: count = sum(1 for l in f if l.startswith("DISCORD_TOKEN_")) print(f" ✓ Discord bot tokens: {count} configured") for w in warnings: print(f" {w}") sys.exit(0) # ─── Sync mode: push to all instances ─── print("\nSyncing credentials to all instances...") # Find all auth-profiles.json patterns = [ f"{home}/.openclaw/agents/*/agent/auth-profiles.json", f"{home}/.openclaw-atomizer/agents/*/agent/auth-profiles.json", ] for pattern in patterns: for path in glob.glob(pattern): try: with open(path) as f: data = json.load(f) changed = False profiles = data.setdefault("profiles", {}) # Sync Anthropic if anthropic_profile and "anthropic:default" in profiles: if profiles["anthropic:default"].get("token") != anthropic_profile.get("token"): profiles["anthropic:default"] = anthropic_profile.copy() changed = True # Sync OpenAI Codex if codex_profile: for key in list(profiles.keys()): if key.startswith("openai-codex:"): if profiles[key].get("refresh") != codex_profile["refresh"]: profiles[key] = codex_profile.copy() changed = True # Sync Google (only for Mario) if "/.openclaw/agents/" in path and google_profile: if "google:default" in profiles: profiles["google:default"] = google_profile.copy() if changed: # Backup before writing backup = path + ".bak" shutil.copy2(path, backup) with open(path, "w") as f: json.dump(data, f, indent=2) agent = path.split("/agents/")[1].split("/")[0] instance = "mario" if "/.openclaw/agents/" in path else "atomizer" updates.append(f"{instance}/{agent}") except Exception as e: warnings.append(f"✗ {path}: {e}") if updates: print(f"\n Updated {len(updates)} profiles:") for u in updates: print(f" ✓ {u}") else: print("\n All profiles already in sync ✓") for w in warnings: print(f"\n {w}") PYEOF # Restart if requested if [ "$MODE" = "--restart" ]; then echo "" CLUSTER="$HOME/atomizer/cluster.sh" if [ -f "$CLUSTER" ]; then echo "Restarting Atomizer cluster..." bash "$CLUSTER" restart fi echo "Restarting Mario gateway..." systemctl --user restart openclaw-gateway.service echo "All instances restarted." fi echo "" echo "Done."