openclaw_oauth_sync/scripts/sync-oauth-token.sh
shamid202 22731fff60 Add complete OAuth token refresh and sync solution
- Setup wizard with auto-detection of OpenClaw paths and Claude CLI
- Token sync watcher (inotifywait) for real-time credential updates
- Auto-refresh trigger timer that runs Claude CLI every 30 min
- Supports Claude CLI in Docker container or on host
- Temporary ANTHROPIC_BASE_URL override for container environments
- Anthropic model configuration for OpenClaw
- Auth profile management (fixes key vs access field)
- Systemd services and timers for both sync and trigger
- Comprehensive documentation and troubleshooting guides
- Re-authentication notification system

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 01:51:18 +07:00

143 lines
4.3 KiB
Bash
Executable File

#!/bin/bash
# sync-oauth-token.sh — Watches Claude CLI credentials and syncs to OpenClaw
# Runs as a long-lived systemd service
#
# When Claude Code CLI auto-refreshes its OAuth token, this script detects
# the file change via inotifywait and syncs the fresh token to:
# 1. OpenClaw's oauth.json (field-mapped)
# 2. OpenClaw's .env (ANTHROPIC_OAUTH_TOKEN)
# 3. Recreates the gateway container (down/up, NOT restart)
set -uo pipefail
# --- Configuration (substituted by setup.sh) ---
CLAUDE_CREDS_FILE="@@CLAUDE_CREDS_FILE@@"
OPENCLAW_OAUTH_FILE="@@OPENCLAW_OAUTH_FILE@@"
OPENCLAW_ENV_FILE="@@OPENCLAW_ENV_FILE@@"
COMPOSE_DIR="@@COMPOSE_DIR@@"
LOG_PREFIX="[sync-oauth-token]"
LAST_SYNC=0
DEBOUNCE_SECONDS=10
# --- Logging ---
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_PREFIX $*"; }
error() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_PREFIX ERROR: $*" >&2; }
sync_token() {
# Debounce: skip if last sync was recent
local now
now=$(date +%s)
local elapsed=$((now - LAST_SYNC))
if [ "$elapsed" -lt "$DEBOUNCE_SECONDS" ]; then
log "Debounce: skipping (last sync ${elapsed}s ago)"
return 0
fi
log "Credential file changed, syncing..."
if [ ! -f "$CLAUDE_CREDS_FILE" ]; then
error "Source file not found: $CLAUDE_CREDS_FILE"
return 1
fi
# Extract and convert fields, write to both targets
python3 -c "
import json, sys, os, re, time
with open('$CLAUDE_CREDS_FILE') as f:
src = json.load(f)
oauth = src.get('claudeAiOauth', {})
access = oauth.get('accessToken', '')
refresh = oauth.get('refreshToken', '')
expires = oauth.get('expiresAt', 0)
scopes = oauth.get('scopes', [])
sub_type = oauth.get('subscriptionType', 'max')
rate_tier = oauth.get('rateLimitTier', 'default_claude_max_5x')
if not access:
print('$LOG_PREFIX ERROR: No access token in source', file=sys.stderr)
sys.exit(1)
# 1. Write OpenClaw oauth.json (field name mapping)
# accessToken -> access
# refreshToken -> refresh
# expiresAt -> expires
openclaw = {
'anthropic': {
'access': access,
'refresh': refresh,
'expires': expires,
'scopes': scopes,
'subscriptionType': sub_type,
'rateLimitTier': rate_tier
}
}
os.makedirs(os.path.dirname('$OPENCLAW_OAUTH_FILE'), exist_ok=True)
with open('$OPENCLAW_OAUTH_FILE', 'w') as f:
json.dump(openclaw, f)
print(f'Updated $OPENCLAW_OAUTH_FILE')
# 2. Update ANTHROPIC_OAUTH_TOKEN in .env
with open('$OPENCLAW_ENV_FILE') as f:
env = f.read()
if 'ANTHROPIC_OAUTH_TOKEN' in env:
env = re.sub(r'ANTHROPIC_OAUTH_TOKEN=.*', f'ANTHROPIC_OAUTH_TOKEN=\"{access}\"', env)
else:
env = env.rstrip('\n') + f'\nANTHROPIC_OAUTH_TOKEN=\"{access}\"\n'
with open('$OPENCLAW_ENV_FILE', 'w') as f:
f.write(env)
print(f'Updated $OPENCLAW_ENV_FILE')
remaining = (expires / 1000 - time.time()) / 3600
print(f'Token: {access[:20]}... expires in {remaining:.1f}h')
"
if [ $? -ne 0 ]; then
error "Failed to sync token"
return 1
fi
# 3. Recreate gateway container to load new env var
# CRITICAL: Must use down/up, NOT restart — restart doesn't reload .env
cd "$COMPOSE_DIR"
log "Recreating gateway container..."
docker compose down openclaw-gateway 2>&1 | while read -r line; do log " $line"; done
docker compose up -d openclaw-gateway 2>&1 | while read -r line; do log " $line"; done
log "Gateway recreated with new token"
LAST_SYNC=$(date +%s)
}
# --- Main ---
log "Starting file watcher on $CLAUDE_CREDS_FILE"
# Verify source file exists
if [ ! -f "$CLAUDE_CREDS_FILE" ]; then
error "Source credentials file not found: $CLAUDE_CREDS_FILE"
error "Make sure Claude Code CLI is installed and authenticated in the container"
exit 1
fi
# Initial sync on startup
sync_token
# Watch for modifications using inotifywait
# Watch the DIRECTORY (not file) to handle atomic rename writes
WATCH_DIR=$(dirname "$CLAUDE_CREDS_FILE")
WATCH_FILE=$(basename "$CLAUDE_CREDS_FILE")
log "Watching directory: $WATCH_DIR for changes to $WATCH_FILE"
while inotifywait -q -e close_write,moved_to "$WATCH_DIR" 2>/dev/null; do
# Small delay to ensure write is complete
sleep 1
if [ -f "$CLAUDE_CREDS_FILE" ]; then
sync_token
fi
done
# If inotifywait exits, log and let systemd restart us
error "inotifywait exited unexpectedly"
exit 1