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:
shamid202
2026-02-27 01:51:18 +07:00
parent 3ae5d5274a
commit 22731fff60
24 changed files with 2846 additions and 6 deletions

153
scripts/fix-auth-profiles.sh Executable file
View 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
View 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
View 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
View 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
View 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
View 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