- 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>
143 lines
4.3 KiB
Bash
Executable File
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
|