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>
This commit is contained in:
153
scripts/fix-auth-profiles.sh
Executable file
153
scripts/fix-auth-profiles.sh
Executable file
@@ -0,0 +1,153 @@
|
||||
#!/bin/bash
|
||||
# fix-auth-profiles.sh — Fix broken Anthropic auth profiles in all OpenClaw agents
|
||||
#
|
||||
# Problem: Auth profiles may have {type:"oauth", key:"sk-ant-oat01-..."}
|
||||
# but OpenClaw's isValidProfile() for type:"oauth" checks for "access" field, not "key"
|
||||
# This causes the profile to be skipped and fall through to env var fallback
|
||||
#
|
||||
# Fix: Change "key" -> "access" field, clear cooldown stats
|
||||
#
|
||||
# Usage:
|
||||
# ./fix-auth-profiles.sh # auto-detect config dir
|
||||
# ./fix-auth-profiles.sh /path/to/.openclaw # custom config dir
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
OPENCLAW_CONFIG_DIR="${1:-}"
|
||||
LOG_PREFIX="[fix-auth-profiles]"
|
||||
|
||||
log() { echo "$LOG_PREFIX $*"; }
|
||||
error() { echo "$LOG_PREFIX ERROR: $*" >&2; }
|
||||
|
||||
# Auto-detect config dir if not provided
|
||||
if [ -z "$OPENCLAW_CONFIG_DIR" ]; then
|
||||
if [ -d "/root/.openclaw/agents" ]; then
|
||||
OPENCLAW_CONFIG_DIR="/root/.openclaw"
|
||||
elif [ -d "$HOME/.openclaw/agents" ]; then
|
||||
OPENCLAW_CONFIG_DIR="$HOME/.openclaw"
|
||||
else
|
||||
error "Cannot find OpenClaw config directory. Provide path as argument."
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
log "Config directory: $OPENCLAW_CONFIG_DIR"
|
||||
|
||||
AGENTS_DIR="$OPENCLAW_CONFIG_DIR/agents"
|
||||
if [ ! -d "$AGENTS_DIR" ]; then
|
||||
error "Agents directory not found: $AGENTS_DIR"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Get current token from .env if available
|
||||
TOKEN=""
|
||||
for env_file in /root/openclaw/.env "$OPENCLAW_CONFIG_DIR/../openclaw/.env"; do
|
||||
if [ -f "$env_file" ]; then
|
||||
TOKEN=$(grep -oP 'ANTHROPIC_OAUTH_TOKEN="\K[^"]+' "$env_file" 2>/dev/null || true)
|
||||
[ -n "$TOKEN" ] && break
|
||||
fi
|
||||
done
|
||||
|
||||
FIXED=0
|
||||
SKIPPED=0
|
||||
|
||||
for agent_dir in "$AGENTS_DIR"/*/agent; do
|
||||
agent=$(basename "$(dirname "$agent_dir")")
|
||||
f="$agent_dir/auth-profiles.json"
|
||||
|
||||
if [ ! -f "$f" ]; then
|
||||
log "SKIP $agent: no auth-profiles.json"
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
log "Checking $agent..."
|
||||
|
||||
python3 -c "
|
||||
import json, sys
|
||||
|
||||
with open('$f') as fh:
|
||||
data = json.load(fh)
|
||||
|
||||
changed = False
|
||||
|
||||
# Fix profile structure
|
||||
if 'anthropic:default' in data.get('profiles', {}):
|
||||
p = data['profiles']['anthropic:default']
|
||||
|
||||
# Ensure type is oauth
|
||||
if p.get('type') != 'oauth':
|
||||
p['type'] = 'oauth'
|
||||
changed = True
|
||||
|
||||
# Ensure provider is anthropic
|
||||
if p.get('provider') != 'anthropic':
|
||||
p['provider'] = 'anthropic'
|
||||
changed = True
|
||||
|
||||
# Move key -> access if needed
|
||||
if 'key' in p and 'access' not in p:
|
||||
p['access'] = p.pop('key')
|
||||
changed = True
|
||||
print(' Fixed: key -> access')
|
||||
elif 'key' in p and 'access' in p:
|
||||
del p['key']
|
||||
changed = True
|
||||
print(' Fixed: removed duplicate key field')
|
||||
|
||||
# Update token if provided
|
||||
token = '$TOKEN'
|
||||
if token and p.get('access') != token:
|
||||
p['access'] = token
|
||||
changed = True
|
||||
print(f' Updated token: {token[:20]}...')
|
||||
|
||||
if not changed:
|
||||
print(' Already correct')
|
||||
else:
|
||||
# Create the profile if it doesn't exist
|
||||
token = '$TOKEN'
|
||||
if token:
|
||||
if 'profiles' not in data:
|
||||
data['profiles'] = {}
|
||||
data['profiles']['anthropic:default'] = {
|
||||
'type': 'oauth',
|
||||
'provider': 'anthropic',
|
||||
'access': token
|
||||
}
|
||||
changed = True
|
||||
print(' Created anthropic:default profile')
|
||||
else:
|
||||
print(' No anthropic:default profile and no token to create one')
|
||||
sys.exit(0)
|
||||
|
||||
# Clear cooldown for anthropic profile
|
||||
if 'usageStats' in data and 'anthropic:default' in data['usageStats']:
|
||||
stats = data['usageStats']['anthropic:default']
|
||||
for key in ['cooldownUntil', 'errorCount', 'failureCounts', 'lastFailureAt']:
|
||||
if key in stats:
|
||||
del stats[key]
|
||||
changed = True
|
||||
if changed:
|
||||
print(' Cleared cooldown stats')
|
||||
|
||||
# Ensure lastGood points to anthropic:default
|
||||
if data.get('lastGood', {}).get('anthropic') != 'anthropic:default':
|
||||
if 'lastGood' not in data:
|
||||
data['lastGood'] = {}
|
||||
data['lastGood']['anthropic'] = 'anthropic:default'
|
||||
changed = True
|
||||
|
||||
if changed:
|
||||
with open('$f', 'w') as fh:
|
||||
json.dump(data, fh, indent=2)
|
||||
print(' Saved')
|
||||
"
|
||||
FIXED=$((FIXED + 1))
|
||||
done
|
||||
|
||||
log ""
|
||||
log "Done. Fixed: $FIXED agents, Skipped: $SKIPPED"
|
||||
log ""
|
||||
log "Restart gateway to apply changes:"
|
||||
log " cd /root/openclaw && docker compose down openclaw-gateway && docker compose up -d openclaw-gateway"
|
||||
125
scripts/refresh-claude-token.sh
Executable file
125
scripts/refresh-claude-token.sh
Executable file
@@ -0,0 +1,125 @@
|
||||
#!/bin/bash
|
||||
# refresh-claude-token.sh — Directly refreshes Claude Max OAuth token via Anthropic API
|
||||
# Fallback method for servers without a Claude CLI container doing auto-refresh
|
||||
# Run via systemd timer every 6 hours
|
||||
#
|
||||
# Endpoint: POST https://console.anthropic.com/v1/oauth/token
|
||||
# Client ID: 9d1c250a-e61b-44d9-88ed-5944d1962f5e (Claude Code public OAuth client)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# --- Configuration (can be overridden via env vars or setup.sh substitution) ---
|
||||
CLAUDE_CREDS_FILE="${CLAUDE_CREDS_FILE:-@@CLAUDE_CREDS_FILE@@}"
|
||||
OPENCLAW_OAUTH_FILE="${OPENCLAW_OAUTH_FILE:-@@OPENCLAW_OAUTH_FILE@@}"
|
||||
OPENCLAW_ENV_FILE="${OPENCLAW_ENV_FILE:-@@OPENCLAW_ENV_FILE@@}"
|
||||
COMPOSE_DIR="${COMPOSE_DIR:-@@COMPOSE_DIR@@}"
|
||||
TOKEN_URL="https://console.anthropic.com/v1/oauth/token"
|
||||
CLIENT_ID="9d1c250a-e61b-44d9-88ed-5944d1962f5e"
|
||||
LOG_PREFIX="[refresh-claude-token]"
|
||||
|
||||
log() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_PREFIX $*"; }
|
||||
error() { echo "$(date '+%Y-%m-%d %H:%M:%S') $LOG_PREFIX ERROR: $*" >&2; }
|
||||
|
||||
# Check credentials file exists (source of current refresh token)
|
||||
if [ ! -f "$CLAUDE_CREDS_FILE" ]; then
|
||||
error "Credentials file not found: $CLAUDE_CREDS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Extract current refresh token
|
||||
REFRESH_TOKEN=$(python3 -c "
|
||||
import json, sys
|
||||
try:
|
||||
with open('$CLAUDE_CREDS_FILE') as f:
|
||||
data = json.load(f)
|
||||
print(data['claudeAiOauth']['refreshToken'])
|
||||
except Exception as e:
|
||||
print(f'ERROR: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
")
|
||||
|
||||
if [ -z "$REFRESH_TOKEN" ]; then
|
||||
error "No refresh token found in $CLAUDE_CREDS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
log "Refreshing token..."
|
||||
|
||||
# Call Anthropic's OAuth token endpoint
|
||||
RESPONSE=$(curl -s -X POST "$TOKEN_URL" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"grant_type\":\"refresh_token\",\"refresh_token\":\"$REFRESH_TOKEN\",\"client_id\":\"$CLIENT_ID\"}")
|
||||
|
||||
# Parse response and write to all credential locations
|
||||
python3 -c "
|
||||
import json, sys, time, os, re
|
||||
|
||||
resp = json.loads('''$RESPONSE''')
|
||||
|
||||
if 'access_token' not in resp:
|
||||
print('$LOG_PREFIX ERROR: Refresh failed:', json.dumps(resp), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
access_token = resp['access_token']
|
||||
refresh_token = resp['refresh_token']
|
||||
expires_at = int(time.time() * 1000) + (resp['expires_in'] * 1000)
|
||||
scopes = resp.get('scope', 'user:inference user:mcp_servers user:profile user:sessions:claude_code').split(' ')
|
||||
|
||||
# 1. Write Claude CLI credentials format (keeps refresh token for next run)
|
||||
claude_creds = {
|
||||
'claudeAiOauth': {
|
||||
'accessToken': access_token,
|
||||
'refreshToken': refresh_token,
|
||||
'expiresAt': expires_at,
|
||||
'scopes': scopes,
|
||||
'subscriptionType': 'max',
|
||||
'rateLimitTier': 'default_claude_max_5x'
|
||||
}
|
||||
}
|
||||
with open('$CLAUDE_CREDS_FILE', 'w') as f:
|
||||
json.dump(claude_creds, f)
|
||||
print(f'Updated $CLAUDE_CREDS_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_token}\"', env)
|
||||
else:
|
||||
env = env.rstrip('\n') + f'\nANTHROPIC_OAUTH_TOKEN=\"{access_token}\"\n'
|
||||
with open('$OPENCLAW_ENV_FILE', 'w') as f:
|
||||
f.write(env)
|
||||
print(f'Updated $OPENCLAW_ENV_FILE')
|
||||
|
||||
# 3. Write OpenClaw oauth.json (field-mapped)
|
||||
openclaw_oauth = {
|
||||
'anthropic': {
|
||||
'access': access_token,
|
||||
'refresh': refresh_token,
|
||||
'expires': expires_at,
|
||||
'scopes': scopes,
|
||||
'subscriptionType': 'max',
|
||||
'rateLimitTier': 'default_claude_max_5x'
|
||||
}
|
||||
}
|
||||
os.makedirs(os.path.dirname('$OPENCLAW_OAUTH_FILE'), exist_ok=True)
|
||||
with open('$OPENCLAW_OAUTH_FILE', 'w') as f:
|
||||
json.dump(openclaw_oauth, f)
|
||||
print(f'Updated $OPENCLAW_OAUTH_FILE')
|
||||
|
||||
expires_hours = resp['expires_in'] // 3600
|
||||
account = resp.get('account', {}).get('email_address', 'unknown')
|
||||
print(f'OK: account={account}, expires in {expires_hours}h, token={access_token[:20]}...')
|
||||
"
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
error "Token refresh failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. Recreate gateway container (down/up, NOT restart)
|
||||
cd "$COMPOSE_DIR"
|
||||
log "Recreating gateway container..."
|
||||
docker compose down openclaw-gateway 2>&1 | sed "s/^/$LOG_PREFIX /"
|
||||
docker compose up -d openclaw-gateway 2>&1 | sed "s/^/$LOG_PREFIX /"
|
||||
log "Gateway recreated with new token"
|
||||
142
scripts/sync-oauth-token.sh
Executable file
142
scripts/sync-oauth-token.sh
Executable file
@@ -0,0 +1,142 @@
|
||||
#!/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
|
||||
112
scripts/trigger-claude-refresh.sh
Executable file
112
scripts/trigger-claude-refresh.sh
Executable file
@@ -0,0 +1,112 @@
|
||||
#!/bin/bash
|
||||
# trigger-claude-refresh.sh — Triggers Claude CLI to refresh OAuth token when near expiry
|
||||
#
|
||||
# Runs via systemd timer every 30 minutes.
|
||||
# Checks token expiry, triggers CLI only when needed.
|
||||
# The existing sync-oauth-token.sh (inotifywait) handles syncing to OpenClaw.
|
||||
#
|
||||
# Supports two modes:
|
||||
# Container mode: Claude CLI runs inside a Docker container
|
||||
# Host mode: Claude CLI is installed directly on the system
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
CREDS_FILE="@@CREDS_FILE@@"
|
||||
REAUTH_FLAG="@@REAUTH_FLAG@@"
|
||||
CLI_MODE="@@CLI_MODE@@"
|
||||
CLI_CONTAINER="@@CLI_CONTAINER@@"
|
||||
CLI_BASE_URL_OVERRIDE="@@CLI_BASE_URL_OVERRIDE@@"
|
||||
LOG_PREFIX="[trigger-refresh]"
|
||||
THRESHOLD_HOURS=1.5
|
||||
TIMEOUT_SECONDS=60
|
||||
|
||||
log() { echo "$LOG_PREFIX $*"; }
|
||||
log_err() { echo "$LOG_PREFIX ERROR: $*" >&2; }
|
||||
|
||||
# Check prerequisites
|
||||
if [ ! -f "$CREDS_FILE" ]; then
|
||||
log_err "Credentials file not found: $CREDS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$CLI_MODE" = "container" ]; then
|
||||
if ! docker ps --filter "name=$CLI_CONTAINER" --format '{{.Names}}' | grep -q "$CLI_CONTAINER"; then
|
||||
log_err "Container $CLI_CONTAINER is not running"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
if ! command -v claude &>/dev/null; then
|
||||
log_err "Claude CLI not found on system"
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# Read expiry and decide whether to trigger
|
||||
REMAINING=$(python3 -c "
|
||||
import json, time
|
||||
with open('$CREDS_FILE') as f:
|
||||
d = json.load(f)
|
||||
expires = d.get('claudeAiOauth', {}).get('expiresAt', 0)
|
||||
remaining = (expires / 1000 - time.time()) / 3600
|
||||
print(f'{remaining:.2f}')
|
||||
")
|
||||
|
||||
log "Token expires in ${REMAINING}h (threshold: ${THRESHOLD_HOURS}h)"
|
||||
|
||||
# Compare as integers (multiply by 100 to avoid bash float issues)
|
||||
REMAINING_X100=$(python3 -c "print(int(float('$REMAINING') * 100))")
|
||||
THRESHOLD_X100=$(python3 -c "print(int(float('$THRESHOLD_HOURS') * 100))")
|
||||
|
||||
if [ "$REMAINING_X100" -gt "$THRESHOLD_X100" ]; then
|
||||
log "Token still fresh, nothing to do"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
log "Token near expiry, triggering Claude CLI refresh..."
|
||||
|
||||
# Record mtime BEFORE
|
||||
MTIME_BEFORE=$(stat -c %Y "$CREDS_FILE" 2>/dev/null || stat -f %m "$CREDS_FILE" 2>/dev/null)
|
||||
|
||||
# Trigger Claude CLI
|
||||
if [ "$CLI_MODE" = "container" ]; then
|
||||
if [ "$CLI_BASE_URL_OVERRIDE" = "true" ]; then
|
||||
CLI_OUTPUT=$(timeout "$TIMEOUT_SECONDS" docker exec \
|
||||
-e ANTHROPIC_BASE_URL=https://api.anthropic.com \
|
||||
"$CLI_CONTAINER" claude -p "say ok" --no-session-persistence 2>&1)
|
||||
else
|
||||
CLI_OUTPUT=$(timeout "$TIMEOUT_SECONDS" docker exec \
|
||||
"$CLI_CONTAINER" claude -p "say ok" --no-session-persistence 2>&1)
|
||||
fi
|
||||
else
|
||||
CLI_OUTPUT=$(timeout "$TIMEOUT_SECONDS" claude -p "say ok" --no-session-persistence 2>&1)
|
||||
fi
|
||||
CLI_EXIT=$?
|
||||
|
||||
if [ "$CLI_EXIT" -eq 124 ]; then
|
||||
log_err "CLI command timed out after ${TIMEOUT_SECONDS}s"
|
||||
fi
|
||||
|
||||
log "CLI exit code: $CLI_EXIT, output: $CLI_OUTPUT"
|
||||
|
||||
# Record mtime AFTER
|
||||
sleep 2
|
||||
MTIME_AFTER=$(stat -c %Y "$CREDS_FILE" 2>/dev/null || stat -f %m "$CREDS_FILE" 2>/dev/null)
|
||||
|
||||
if [ "$MTIME_BEFORE" != "$MTIME_AFTER" ]; then
|
||||
log "Token refreshed successfully (mtime changed: $MTIME_BEFORE -> $MTIME_AFTER)"
|
||||
|
||||
if [ -f "$REAUTH_FLAG" ]; then
|
||||
rm -f "$REAUTH_FLAG"
|
||||
log "Cleared previous REAUTH_NEEDED flag"
|
||||
fi
|
||||
|
||||
exit 0
|
||||
else
|
||||
log_err "Token refresh FAILED — credentials.json was not updated"
|
||||
log_err "Re-authentication may be required (refresh token expired or subscription issue)"
|
||||
|
||||
echo "Re-authentication needed at $(date -u '+%Y-%m-%dT%H:%M:%SZ')" > "$REAUTH_FLAG"
|
||||
log_err "Wrote $REAUTH_FLAG"
|
||||
|
||||
exit 1
|
||||
fi
|
||||
46
scripts/uninstall.sh
Executable file
46
scripts/uninstall.sh
Executable file
@@ -0,0 +1,46 @@
|
||||
#!/bin/bash
|
||||
# uninstall.sh — Remove the OAuth token sync service
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
LOG_PREFIX="[uninstall]"
|
||||
log() { echo "$LOG_PREFIX $*"; }
|
||||
|
||||
echo ""
|
||||
echo "Removing OAuth Token Sync for OpenClaw"
|
||||
echo "======================================="
|
||||
echo ""
|
||||
|
||||
# Stop and disable systemd services
|
||||
for svc in sync-oauth-token.service refresh-claude-token.service refresh-claude-token.timer trigger-claude-refresh.service trigger-claude-refresh.timer; do
|
||||
if systemctl is-active --quiet "$svc" 2>/dev/null; then
|
||||
log "Stopping $svc..."
|
||||
systemctl stop "$svc"
|
||||
fi
|
||||
if systemctl is-enabled --quiet "$svc" 2>/dev/null; then
|
||||
log "Disabling $svc..."
|
||||
systemctl disable "$svc"
|
||||
fi
|
||||
if [ -f "/etc/systemd/system/$svc" ]; then
|
||||
log "Removing /etc/systemd/system/$svc"
|
||||
rm -f "/etc/systemd/system/$svc"
|
||||
fi
|
||||
done
|
||||
|
||||
systemctl daemon-reload 2>/dev/null
|
||||
|
||||
# Remove installed scripts
|
||||
for script in /usr/local/bin/sync-oauth-token.sh /usr/local/bin/refresh-claude-token.sh /usr/local/bin/trigger-claude-refresh.sh; do
|
||||
if [ -f "$script" ]; then
|
||||
log "Removing $script"
|
||||
rm -f "$script"
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
log "Done. The following files were NOT removed (contain your credentials):"
|
||||
log " - /root/.openclaw/credentials/oauth.json"
|
||||
log " - /root/openclaw/.env (ANTHROPIC_OAUTH_TOKEN)"
|
||||
log " - /root/.openclaw/agents/*/agent/auth-profiles.json"
|
||||
echo ""
|
||||
log "To fully clean up, remove those manually if needed."
|
||||
172
scripts/verify.sh
Executable file
172
scripts/verify.sh
Executable file
@@ -0,0 +1,172 @@
|
||||
#!/bin/bash
|
||||
# verify.sh — Post-install health check for OAuth token sync
|
||||
# Run anytime to check if everything is working correctly
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
RED='\033[0;31m'
|
||||
GREEN='\033[0;32m'
|
||||
YELLOW='\033[1;33m'
|
||||
NC='\033[0m'
|
||||
|
||||
pass() { echo -e " ${GREEN}[PASS]${NC} $*"; }
|
||||
fail() { echo -e " ${RED}[FAIL]${NC} $*"; }
|
||||
warn() { echo -e " ${YELLOW}[WARN]${NC} $*"; }
|
||||
|
||||
ERRORS=0
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " OAuth Token Sync — Health Check"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
|
||||
# --- 1. Systemd service ---
|
||||
echo "1. Systemd service status"
|
||||
if systemctl is-active --quiet sync-oauth-token.service 2>/dev/null; then
|
||||
pass "sync-oauth-token.service is active"
|
||||
else
|
||||
fail "sync-oauth-token.service is not running"
|
||||
echo " Fix: systemctl start sync-oauth-token.service"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
|
||||
if systemctl is-enabled --quiet sync-oauth-token.service 2>/dev/null; then
|
||||
pass "Service is enabled (starts on boot)"
|
||||
else
|
||||
warn "Service is not enabled for boot"
|
||||
echo " Fix: systemctl enable sync-oauth-token.service"
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# --- 2. inotifywait process ---
|
||||
echo "2. File watcher process"
|
||||
if pgrep -f inotifywait > /dev/null 2>&1; then
|
||||
WATCH_PATH=$(pgrep -af inotifywait | grep -oP '/[^ ]+' | tail -1)
|
||||
pass "inotifywait is running (watching: $WATCH_PATH)"
|
||||
else
|
||||
fail "inotifywait is not running"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# --- 3. Source credentials file ---
|
||||
echo "3. Claude CLI credentials file"
|
||||
# Try to find the watched file from the service
|
||||
SYNC_SCRIPT=$(which sync-oauth-token.sh 2>/dev/null || echo "/usr/local/bin/sync-oauth-token.sh")
|
||||
if [ -f "$SYNC_SCRIPT" ]; then
|
||||
SOURCE_FILE=$(grep 'CLAUDE_CREDS_FILE=' "$SYNC_SCRIPT" | head -1 | cut -d'"' -f2)
|
||||
fi
|
||||
SOURCE_FILE="${SOURCE_FILE:-/root/.openclaw/workspaces/workspace-claude-proxy/config/.claude/.credentials.json}"
|
||||
|
||||
if [ -f "$SOURCE_FILE" ]; then
|
||||
pass "File exists: $SOURCE_FILE"
|
||||
EXPIRES=$(python3 -c "
|
||||
import json, time
|
||||
with open('$SOURCE_FILE') as f:
|
||||
d = json.load(f)
|
||||
exp = d['claudeAiOauth']['expiresAt'] / 1000
|
||||
remaining = (exp - time.time()) / 3600
|
||||
status = 'VALID' if remaining > 0 else 'EXPIRED'
|
||||
print(f'{remaining:.1f}h remaining ({status})')
|
||||
" 2>/dev/null || echo "parse error")
|
||||
if echo "$EXPIRES" | grep -q "VALID"; then
|
||||
pass "Token: $EXPIRES"
|
||||
else
|
||||
fail "Token: $EXPIRES"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
fail "File not found: $SOURCE_FILE"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# --- 4. OpenClaw oauth.json ---
|
||||
echo "4. OpenClaw oauth.json"
|
||||
for path in /root/.openclaw/credentials/oauth.json /home/*/.openclaw/credentials/oauth.json; do
|
||||
if [ -f "$path" ]; then
|
||||
OAUTH_FILE="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "${OAUTH_FILE:-}" ] && [ -f "$OAUTH_FILE" ]; then
|
||||
HAS_ACCESS=$(python3 -c "
|
||||
import json
|
||||
with open('$OAUTH_FILE') as f:
|
||||
d = json.load(f)
|
||||
a = d.get('anthropic', {})
|
||||
print('yes' if a.get('access') else 'no')
|
||||
" 2>/dev/null || echo "no")
|
||||
if [ "$HAS_ACCESS" = "yes" ]; then
|
||||
pass "oauth.json exists with anthropic.access field: $OAUTH_FILE"
|
||||
else
|
||||
fail "oauth.json exists but missing anthropic.access field"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
fail "oauth.json not found"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# --- 5. .env file ---
|
||||
echo "5. Environment file (.env)"
|
||||
for path in /root/openclaw/.env; do
|
||||
if [ -f "$path" ]; then
|
||||
ENV_FILE="$path"
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -n "${ENV_FILE:-}" ] && [ -f "$ENV_FILE" ]; then
|
||||
if grep -q 'ANTHROPIC_OAUTH_TOKEN=' "$ENV_FILE"; then
|
||||
TOKEN_PREFIX=$(grep 'ANTHROPIC_OAUTH_TOKEN=' "$ENV_FILE" | head -1 | cut -d'"' -f2 | cut -c1-20)
|
||||
pass ".env has ANTHROPIC_OAUTH_TOKEN: ${TOKEN_PREFIX}..."
|
||||
else
|
||||
fail ".env missing ANTHROPIC_OAUTH_TOKEN"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
else
|
||||
fail ".env file not found"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# --- 6. Gateway container ---
|
||||
echo "6. Gateway container"
|
||||
GATEWAY=$(docker ps --filter name=openclaw --format '{{.Names}}' 2>/dev/null | grep gateway | head -1)
|
||||
if [ -n "$GATEWAY" ]; then
|
||||
UPTIME=$(docker ps --filter "name=$GATEWAY" --format '{{.Status}}' 2>/dev/null)
|
||||
pass "Gateway running: $GATEWAY ($UPTIME)"
|
||||
|
||||
# Check container env var matches .env
|
||||
CONTAINER_TOKEN=$(docker exec "$GATEWAY" printenv ANTHROPIC_OAUTH_TOKEN 2>/dev/null | cut -c1-20)
|
||||
if [ -n "$CONTAINER_TOKEN" ]; then
|
||||
pass "Container has ANTHROPIC_OAUTH_TOKEN: ${CONTAINER_TOKEN}..."
|
||||
else
|
||||
warn "Container missing ANTHROPIC_OAUTH_TOKEN env var"
|
||||
fi
|
||||
else
|
||||
fail "No OpenClaw gateway container found"
|
||||
ERRORS=$((ERRORS + 1))
|
||||
fi
|
||||
echo ""
|
||||
|
||||
# --- Summary ---
|
||||
echo "=========================================="
|
||||
if [ "$ERRORS" -eq 0 ]; then
|
||||
echo -e " ${GREEN}All checks passed${NC}"
|
||||
else
|
||||
echo -e " ${RED}$ERRORS check(s) failed${NC}"
|
||||
fi
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Useful commands:"
|
||||
echo " journalctl -u sync-oauth-token.service -f # Watch sync logs"
|
||||
echo " systemctl restart sync-oauth-token.service # Force re-sync"
|
||||
echo " ./scripts/verify.sh # Run this check again"
|
||||
echo ""
|
||||
|
||||
exit $ERRORS
|
||||
Reference in New Issue
Block a user