openclaw_oauth_sync/setup.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

853 lines
30 KiB
Bash
Executable File

#!/bin/bash
# ============================================================================
# OAuth Fix for OpenClaw — Interactive Setup Wizard
# ============================================================================
# Configures automatic Anthropic OAuth token refresh for OpenClaw.
# Detects paths, installs the sync service, and configures the Anthropic model.
#
# Usage:
# ./setup.sh # Interactive mode
# ./setup.sh --uninstall # Remove everything
# ============================================================================
set -euo pipefail
VERSION="1.0.0"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# --- Colors ---
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
NC='\033[0m'
info() { echo -e "${BLUE}[INFO]${NC} $*"; }
success() { echo -e "${GREEN}[OK]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
header() { echo -e "\n${BOLD}${CYAN}━━━ $* ━━━${NC}\n"; }
ask() {
local prompt="$1"
local default="${2:-}"
local result
if [ -n "$default" ]; then
read -rp "$(echo -e "${BOLD}$prompt${NC} [$default]: ")" result
echo "${result:-$default}"
else
read -rp "$(echo -e "${BOLD}$prompt${NC}: ")" result
echo "$result"
fi
}
confirm() {
local prompt="$1"
local default="${2:-Y}"
local result
read -rp "$(echo -e "${BOLD}$prompt${NC} [${default}]: ")" result
result="${result:-$default}"
[[ "$result" =~ ^[Yy] ]]
}
# --- Uninstall ---
if [ "${1:-}" = "--uninstall" ]; then
bash "$SCRIPT_DIR/scripts/uninstall.sh"
exit $?
fi
# ============================================================================
# BANNER
# ============================================================================
echo ""
echo -e "${BOLD}${CYAN}"
echo " ╔══════════════════════════════════════════════════════╗"
echo " ║ OAuth Fix for OpenClaw + Claude Max ║"
echo " ║ Automatic Anthropic Token Refresh ║"
echo " ║ v${VERSION}"
echo " ╚══════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo -e "${DIM} Keeps your Anthropic OAuth tokens fresh by syncing"
echo -e " Claude Code CLI's auto-refreshed credentials to OpenClaw.${NC}"
echo ""
# ============================================================================
# STEP 1: Prerequisites
# ============================================================================
header "Step 1: Checking Prerequisites"
MISSING=0
for cmd in docker python3 curl systemctl; do
if command -v "$cmd" &>/dev/null; then
success "$cmd found"
else
error "$cmd not found"
MISSING=$((MISSING + 1))
fi
done
# Check docker compose (v2)
if docker compose version &>/dev/null; then
success "docker compose found ($(docker compose version --short 2>/dev/null || echo 'v2'))"
else
error "docker compose v2 not found"
MISSING=$((MISSING + 1))
fi
# Check inotifywait
if command -v inotifywait &>/dev/null; then
success "inotifywait found"
USE_INOTIFY=true
else
warn "inotifywait not found (inotify-tools package)"
echo -e " ${DIM}Install with: apt install inotify-tools${NC}"
if confirm " Install inotify-tools now?" "Y"; then
apt-get install -y inotify-tools 2>&1 | tail -1
if command -v inotifywait &>/dev/null; then
success "inotifywait installed"
USE_INOTIFY=true
else
warn "Installation failed. Will use timer-based fallback."
USE_INOTIFY=false
fi
else
warn "Will use timer-based fallback (6-hour refresh cycle)"
USE_INOTIFY=false
fi
fi
if [ "$MISSING" -gt 0 ]; then
error "$MISSING prerequisite(s) missing. Please install them and re-run."
exit 1
fi
# ============================================================================
# STEP 2: Detect OpenClaw Installation
# ============================================================================
header "Step 2: Detecting OpenClaw Installation"
# Find openclaw.json
OPENCLAW_CONFIG_DIR=""
for path in \
"${OPENCLAW_CONFIG_DIR:-}" \
/root/.openclaw \
/home/*/.openclaw; do
if [ -f "$path/openclaw.json" ] 2>/dev/null; then
OPENCLAW_CONFIG_DIR="$path"
break
fi
done
if [ -z "$OPENCLAW_CONFIG_DIR" ]; then
# Try docker inspect
GATEWAY=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i 'openclaw.*gateway' | head -1)
if [ -n "$GATEWAY" ]; then
OPENCLAW_CONFIG_DIR=$(docker inspect "$GATEWAY" --format '{{range .Mounts}}{{if eq .Destination "/home/node/.openclaw"}}{{.Source}}{{end}}{{end}}' 2>/dev/null)
fi
fi
if [ -z "$OPENCLAW_CONFIG_DIR" ] || [ ! -f "$OPENCLAW_CONFIG_DIR/openclaw.json" ]; then
error "Cannot find OpenClaw installation"
OPENCLAW_CONFIG_DIR=$(ask "Enter OpenClaw config directory (contains openclaw.json)")
if [ ! -f "$OPENCLAW_CONFIG_DIR/openclaw.json" ]; then
error "openclaw.json not found in $OPENCLAW_CONFIG_DIR"
exit 1
fi
fi
success "Config: $OPENCLAW_CONFIG_DIR/openclaw.json"
# Find docker-compose directory
COMPOSE_DIR=""
for path in \
/root/openclaw \
"$(dirname "$OPENCLAW_CONFIG_DIR")/openclaw" \
/opt/openclaw; do
if [ -f "$path/docker-compose.yml" ] || [ -f "$path/docker-compose.yaml" ] || [ -f "$path/compose.yml" ]; then
COMPOSE_DIR="$path"
break
fi
done
if [ -z "$COMPOSE_DIR" ]; then
COMPOSE_DIR=$(ask "Enter OpenClaw docker-compose directory" "/root/openclaw")
fi
success "Compose: $COMPOSE_DIR"
# Find .env file
ENV_FILE="$COMPOSE_DIR/.env"
if [ ! -f "$ENV_FILE" ]; then
warn ".env not found at $ENV_FILE"
ENV_FILE=$(ask "Enter .env file path" "$COMPOSE_DIR/.env")
fi
success "Env: $ENV_FILE"
# Find gateway container
GATEWAY_CONTAINER=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i 'openclaw.*gateway' | head -1)
if [ -z "$GATEWAY_CONTAINER" ]; then
GATEWAY_CONTAINER=$(ask "Enter gateway container name" "openclaw-openclaw-gateway-1")
fi
success "Gateway: $GATEWAY_CONTAINER"
# Derive oauth.json path
OPENCLAW_OAUTH_FILE="$OPENCLAW_CONFIG_DIR/credentials/oauth.json"
success "OAuth file: $OPENCLAW_OAUTH_FILE"
# ============================================================================
# STEP 3: Detect Claude CLI Credentials
# ============================================================================
header "Step 3: Detecting Claude CLI Credentials"
CLAUDE_CREDS_FILE=""
# Strategy 1: Find claude-proxy or similar container
CLAUDE_CONTAINER=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i 'claude' | head -1)
if [ -n "$CLAUDE_CONTAINER" ]; then
info "Found Claude container: $CLAUDE_CONTAINER"
# Get its volume mounts and find credentials
MOUNT_SOURCE=$(docker inspect "$CLAUDE_CONTAINER" --format '{{range .Mounts}}{{if or (eq .Destination "/root") (eq .Destination "/root/.claude") (eq .Destination "/home/node/.claude")}}{{.Source}}{{end}}{{end}}' 2>/dev/null)
if [ -n "$MOUNT_SOURCE" ]; then
for suffix in "/.claude/.credentials.json" "/.credentials.json"; do
if [ -f "${MOUNT_SOURCE}${suffix}" ]; then
CLAUDE_CREDS_FILE="${MOUNT_SOURCE}${suffix}"
break
fi
done
fi
fi
# Strategy 2: Search workspace directories
if [ -z "$CLAUDE_CREDS_FILE" ]; then
for path in "$OPENCLAW_CONFIG_DIR"/workspaces/*/config/.claude/.credentials.json; do
if [ -f "$path" ]; then
CLAUDE_CREDS_FILE="$path"
break
fi
done
fi
# Strategy 3: Direct paths
if [ -z "$CLAUDE_CREDS_FILE" ]; then
for path in \
/root/.claude/.credentials.json \
"$HOME/.claude/.credentials.json"; do
if [ -f "$path" ]; then
CLAUDE_CREDS_FILE="$path"
break
fi
done
fi
if [ -z "$CLAUDE_CREDS_FILE" ] || [ ! -f "$CLAUDE_CREDS_FILE" ]; then
warn "Could not auto-detect Claude CLI credentials"
CLAUDE_CREDS_FILE=$(ask "Enter path to Claude CLI .credentials.json")
if [ ! -f "$CLAUDE_CREDS_FILE" ]; then
error "File not found: $CLAUDE_CREDS_FILE"
exit 1
fi
fi
# Validate credentials
TOKEN_INFO=$(python3 -c "
import json, time
with open('$CLAUDE_CREDS_FILE') as f:
d = json.load(f)
oauth = d.get('claudeAiOauth', {})
access = oauth.get('accessToken', '')
refresh = oauth.get('refreshToken', '')
expires = oauth.get('expiresAt', 0)
remaining = (expires / 1000 - time.time()) / 3600
status = 'VALID' if remaining > 0 else 'EXPIRED'
print(f'access={access[:20]}... refresh={\"yes\" if refresh else \"no\"} remaining={remaining:.1f}h status={status}')
" 2>/dev/null || echo "error")
if echo "$TOKEN_INFO" | grep -q "error"; then
error "Cannot parse credentials file. Is it a valid Claude CLI .credentials.json?"
exit 1
fi
success "Credentials: $CLAUDE_CREDS_FILE"
info " $TOKEN_INFO"
CURRENT_TOKEN=$(python3 -c "
import json
with open('$CLAUDE_CREDS_FILE') as f:
d = json.load(f)
print(d['claudeAiOauth']['accessToken'])
" 2>/dev/null)
# ============================================================================
# STEP 4: Confirm Configuration
# ============================================================================
header "Step 4: Configuration Summary"
echo -e " ${BOLD}OpenClaw config:${NC} $OPENCLAW_CONFIG_DIR"
echo -e " ${BOLD}Docker compose:${NC} $COMPOSE_DIR"
echo -e " ${BOLD}Environment file:${NC} $ENV_FILE"
echo -e " ${BOLD}Gateway container:${NC} $GATEWAY_CONTAINER"
echo -e " ${BOLD}OAuth file:${NC} $OPENCLAW_OAUTH_FILE"
echo -e " ${BOLD}Claude CLI creds:${NC} $CLAUDE_CREDS_FILE"
echo -e " ${BOLD}Sync method:${NC} $([ "$USE_INOTIFY" = true ] && echo 'inotifywait (real-time)' || echo 'systemd timer (every 6h)')"
echo ""
if ! confirm "Proceed with these settings?" "Y"; then
echo "Aborted."
exit 0
fi
# ============================================================================
# STEP 5: Configure Anthropic Model in OpenClaw
# ============================================================================
header "Step 5: Configuring Anthropic Model"
# Check if anthropic model is already configured
HAS_ANTHROPIC=$(python3 -c "
import json
with open('$OPENCLAW_CONFIG_DIR/openclaw.json') as f:
d = json.load(f)
primary = d.get('agents', {}).get('defaults', {}).get('model', {}).get('primary', '')
print('yes' if 'anthropic/' in primary else 'no')
" 2>/dev/null || echo "no")
if [ "$HAS_ANTHROPIC" = "yes" ]; then
success "Anthropic model already configured as primary"
else
info "Anthropic model not yet configured"
if confirm " Set anthropic/claude-opus-4-6 as primary model?" "Y"; then
python3 -c "
import json
with open('$OPENCLAW_CONFIG_DIR/openclaw.json') as f:
d = json.load(f)
# Set primary model
if 'agents' not in d:
d['agents'] = {}
if 'defaults' not in d['agents']:
d['agents']['defaults'] = {}
if 'model' not in d['agents']['defaults']:
d['agents']['defaults']['model'] = {}
d['agents']['defaults']['model']['primary'] = 'anthropic/claude-opus-4-6'
# Add fallback if not present
fallbacks = d['agents']['defaults']['model'].get('fallbacks', [])
if 'anthropic/claude-sonnet-4-6' not in fallbacks:
fallbacks.insert(0, 'anthropic/claude-sonnet-4-6')
d['agents']['defaults']['model']['fallbacks'] = fallbacks
# Add model aliases
if 'models' not in d['agents']['defaults']:
d['agents']['defaults']['models'] = {}
d['agents']['defaults']['models']['anthropic/claude-opus-4-6'] = {'alias': 'Claude Opus 4.6 (Max)'}
d['agents']['defaults']['models']['anthropic/claude-sonnet-4-6'] = {'alias': 'Claude Sonnet 4.6 (Max)'}
with open('$OPENCLAW_CONFIG_DIR/openclaw.json', 'w') as f:
json.dump(d, f, indent=2)
"
success "Set anthropic/claude-opus-4-6 as primary model"
success "Added anthropic/claude-sonnet-4-6 to fallbacks"
success "Added model aliases"
fi
fi
# Check for broken custom anthropic provider (common mistake)
HAS_CUSTOM_PROVIDER=$(python3 -c "
import json
with open('$OPENCLAW_CONFIG_DIR/openclaw.json') as f:
d = json.load(f)
providers = d.get('models', {}).get('providers', {})
print('yes' if 'anthropic' in providers else 'no')
" 2>/dev/null || echo "no")
if [ "$HAS_CUSTOM_PROVIDER" = "yes" ]; then
warn "Found custom 'anthropic' in models.providers — this causes 404 errors!"
warn "The built-in provider handles Anthropic. Custom entry causes double /v1 in URLs."
if confirm " Remove the custom anthropic provider entry?" "Y"; then
python3 -c "
import json
with open('$OPENCLAW_CONFIG_DIR/openclaw.json') as f:
d = json.load(f)
del d['models']['providers']['anthropic']
with open('$OPENCLAW_CONFIG_DIR/openclaw.json', 'w') as f:
json.dump(d, f, indent=2)
"
success "Removed custom anthropic provider"
fi
fi
# Add ANTHROPIC_OAUTH_TOKEN to .env if missing
if grep -q 'ANTHROPIC_OAUTH_TOKEN=' "$ENV_FILE" 2>/dev/null; then
success "ANTHROPIC_OAUTH_TOKEN already in .env"
else
echo "ANTHROPIC_OAUTH_TOKEN=\"$CURRENT_TOKEN\"" >> "$ENV_FILE"
success "Added ANTHROPIC_OAUTH_TOKEN to .env"
fi
# ============================================================================
# STEP 6: Fix/Create Auth Profiles
# ============================================================================
header "Step 6: Configuring Auth Profiles"
AGENTS_DIR="$OPENCLAW_CONFIG_DIR/agents"
if [ -d "$AGENTS_DIR" ]; then
for agent_dir in "$AGENTS_DIR"/*/agent; do
[ -d "$agent_dir" ] || continue
agent=$(basename "$(dirname "$agent_dir")")
f="$agent_dir/auth-profiles.json"
if [ ! -f "$f" ]; then
# Create new auth-profiles.json
python3 -c "
import json
data = {
'version': 1,
'profiles': {
'anthropic:default': {
'type': 'oauth',
'provider': 'anthropic',
'access': '$CURRENT_TOKEN'
}
},
'lastGood': {
'anthropic': 'anthropic:default'
},
'usageStats': {}
}
with open('$f', 'w') as fh:
json.dump(data, fh, indent=2)
"
success "$agent: created auth-profiles.json"
else
# Fix existing profile
python3 -c "
import json
with open('$f') as fh:
data = json.load(fh)
changed = False
if 'profiles' not in data:
data['profiles'] = {}
p = data['profiles'].get('anthropic:default', {})
if not p:
data['profiles']['anthropic:default'] = {
'type': 'oauth',
'provider': 'anthropic',
'access': '$CURRENT_TOKEN'
}
changed = True
else:
if p.get('type') != 'oauth':
p['type'] = 'oauth'
changed = True
if 'key' in p and 'access' not in p:
p['access'] = p.pop('key')
changed = True
elif 'key' in p:
del p['key']
changed = True
if p.get('access') != '$CURRENT_TOKEN':
p['access'] = '$CURRENT_TOKEN'
changed = True
# Clear cooldown
stats = data.get('usageStats', {}).get('anthropic:default', {})
for k in ['cooldownUntil', 'errorCount', 'failureCounts', 'lastFailureAt']:
if k in stats:
del stats[k]
changed = True
if 'lastGood' not in data:
data['lastGood'] = {}
data['lastGood']['anthropic'] = 'anthropic:default'
if changed:
with open('$f', 'w') as fh:
json.dump(data, fh, indent=2)
print('fixed')
else:
print('ok')
" 2>/dev/null
RESULT=$(python3 -c "
import json
with open('$f') as fh:
data = json.load(fh)
p = data.get('profiles', {}).get('anthropic:default', {})
has_access = 'access' in p
print(f'type={p.get(\"type\",\"none\")} access={\"yes\" if has_access else \"no\"}')
" 2>/dev/null)
success "$agent: $RESULT"
fi
done
else
warn "No agents directory found at $AGENTS_DIR"
fi
# ============================================================================
# STEP 7: Install Sync Script
# ============================================================================
header "Step 7: Installing Sync Service"
INSTALL_DIR="/usr/local/bin"
if [ "$USE_INOTIFY" = true ]; then
# Install inotify-based watcher
SYNC_SCRIPT="$INSTALL_DIR/sync-oauth-token.sh"
sed \
-e "s|@@CLAUDE_CREDS_FILE@@|$CLAUDE_CREDS_FILE|g" \
-e "s|@@OPENCLAW_OAUTH_FILE@@|$OPENCLAW_OAUTH_FILE|g" \
-e "s|@@OPENCLAW_ENV_FILE@@|$ENV_FILE|g" \
-e "s|@@COMPOSE_DIR@@|$COMPOSE_DIR|g" \
"$SCRIPT_DIR/scripts/sync-oauth-token.sh" > "$SYNC_SCRIPT"
chmod +x "$SYNC_SCRIPT"
success "Installed $SYNC_SCRIPT"
# Install systemd service
sed "s|@@SYNC_SCRIPT_PATH@@|$SYNC_SCRIPT|g" \
"$SCRIPT_DIR/templates/sync-oauth-token.service" > /etc/systemd/system/sync-oauth-token.service
success "Installed systemd service"
systemctl daemon-reload
systemctl enable sync-oauth-token.service
systemctl start sync-oauth-token.service
success "Service started and enabled"
else
# Install timer-based refresh
REFRESH_SCRIPT="$INSTALL_DIR/refresh-claude-token.sh"
sed \
-e "s|@@CLAUDE_CREDS_FILE@@|$CLAUDE_CREDS_FILE|g" \
-e "s|@@OPENCLAW_OAUTH_FILE@@|$OPENCLAW_OAUTH_FILE|g" \
-e "s|@@OPENCLAW_ENV_FILE@@|$ENV_FILE|g" \
-e "s|@@COMPOSE_DIR@@|$COMPOSE_DIR|g" \
"$SCRIPT_DIR/scripts/refresh-claude-token.sh" > "$REFRESH_SCRIPT"
chmod +x "$REFRESH_SCRIPT"
success "Installed $REFRESH_SCRIPT"
# Install systemd timer
sed "s|@@REFRESH_SCRIPT_PATH@@|$REFRESH_SCRIPT|g" \
"$SCRIPT_DIR/templates/refresh-claude-token.service" > /etc/systemd/system/refresh-claude-token.service
cp "$SCRIPT_DIR/templates/refresh-claude-token.timer" /etc/systemd/system/
success "Installed systemd timer"
systemctl daemon-reload
systemctl enable refresh-claude-token.timer
systemctl start refresh-claude-token.timer
success "Timer started and enabled (runs every 6 hours)"
fi
# ============================================================================
# STEP 8: Detect Claude CLI & Install Auto-Refresh Trigger
# ============================================================================
header "Step 8: Detecting Claude CLI & Installing Auto-Refresh Trigger"
info "The sync service watches for credential changes, but something must"
info "trigger Claude CLI to actually refresh the token before it expires."
info "This step installs a timer that triggers the refresh automatically."
echo ""
# Detect Claude CLI — container or host?
CLI_MODE=""
CLI_CONTAINER=""
CLI_BASE_URL_OVERRIDE="false"
# Strategy 1: Check for Claude container
CLAUDE_CONTAINER_FOUND=$(docker ps --format '{{.Names}}' 2>/dev/null | grep -i 'claude' | head -1)
if [ -n "$CLAUDE_CONTAINER_FOUND" ]; then
# Verify claude binary exists inside
if docker exec "$CLAUDE_CONTAINER_FOUND" which claude &>/dev/null; then
CLI_MODE="container"
CLI_CONTAINER="$CLAUDE_CONTAINER_FOUND"
info "Found Claude CLI in container: $CLI_CONTAINER"
# Check if ANTHROPIC_BASE_URL needs override
CTR_BASE_URL=$(docker inspect "$CLI_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^ANTHROPIC_BASE_URL=' | cut -d= -f2-)
if [ -n "$CTR_BASE_URL" ] && [ "$CTR_BASE_URL" != "https://api.anthropic.com" ]; then
CLI_BASE_URL_OVERRIDE="true"
info "Container has ANTHROPIC_BASE_URL=$CTR_BASE_URL"
info "Will use temporary override for CLI invocations (does not affect container)"
fi
fi
fi
# Strategy 2: Check host
if [ -z "$CLI_MODE" ]; then
if command -v claude &>/dev/null; then
CLI_MODE="host"
info "Found Claude CLI on host: $(which claude)"
fi
fi
# Strategy 3: Ask user
if [ -z "$CLI_MODE" ]; then
warn "Could not auto-detect Claude CLI"
echo ""
echo " Where is Claude CLI installed?"
echo " 1) In a Docker container"
echo " 2) Directly on this system"
echo " 3) Skip (no auto-refresh trigger)"
CHOICE=$(ask "Select" "3")
case "$CHOICE" in
1)
CLI_MODE="container"
CLI_CONTAINER=$(ask "Enter container name" "claude-proxy")
if ! docker exec "$CLI_CONTAINER" which claude &>/dev/null; then
error "Claude CLI not found in container $CLI_CONTAINER"
CLI_MODE=""
else
# Check base URL
CTR_BASE_URL=$(docker inspect "$CLI_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^ANTHROPIC_BASE_URL=' | cut -d= -f2-)
if [ -n "$CTR_BASE_URL" ] && [ "$CTR_BASE_URL" != "https://api.anthropic.com" ]; then
CLI_BASE_URL_OVERRIDE="true"
fi
fi
;;
2)
CLI_MODE="host"
if ! command -v claude &>/dev/null; then
error "Claude CLI not found on system"
CLI_MODE=""
fi
;;
*)
warn "Skipping auto-refresh trigger installation"
;;
esac
fi
TRIGGER_INSTALLED=false
if [ -n "$CLI_MODE" ]; then
# Test Claude CLI invocation
info "Testing Claude CLI invocation..."
if [ "$CLI_MODE" = "container" ]; then
if [ "$CLI_BASE_URL_OVERRIDE" = "true" ]; then
TEST_CMD="docker exec -e ANTHROPIC_BASE_URL=https://api.anthropic.com $CLI_CONTAINER claude -p 'say ok' --no-session-persistence"
TEST_OUTPUT=$(timeout 60 docker exec -e ANTHROPIC_BASE_URL=https://api.anthropic.com "$CLI_CONTAINER" claude -p "say ok" --no-session-persistence 2>&1)
else
TEST_CMD="docker exec $CLI_CONTAINER claude -p 'say ok' --no-session-persistence"
TEST_OUTPUT=$(timeout 60 docker exec "$CLI_CONTAINER" claude -p "say ok" --no-session-persistence 2>&1)
fi
else
TEST_CMD="claude -p 'say ok' --no-session-persistence"
TEST_OUTPUT=$(timeout 60 claude -p "say ok" --no-session-persistence 2>&1)
fi
TEST_EXIT=$?
if [ "$TEST_EXIT" -eq 0 ] && [ -n "$TEST_OUTPUT" ]; then
success "CLI test passed: $TEST_OUTPUT"
# Verify proxy/container env unchanged (if container mode)
if [ "$CLI_MODE" = "container" ] && [ "$CLI_BASE_URL_OVERRIDE" = "true" ]; then
VERIFY_URL=$(docker inspect "$CLI_CONTAINER" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null | grep '^ANTHROPIC_BASE_URL=' | cut -d= -f2-)
if [ "$VERIFY_URL" = "$CTR_BASE_URL" ]; then
success "Container env unchanged (temporary override confirmed)"
else
warn "Container env may have changed — check manually"
fi
fi
# Install trigger script
TRIGGER_SCRIPT="$INSTALL_DIR/trigger-claude-refresh.sh"
REAUTH_FLAG="$COMPOSE_DIR/REAUTH_NEEDED"
sed \
-e "s|@@CREDS_FILE@@|$CLAUDE_CREDS_FILE|g" \
-e "s|@@REAUTH_FLAG@@|$REAUTH_FLAG|g" \
-e "s|@@CLI_MODE@@|$CLI_MODE|g" \
-e "s|@@CLI_CONTAINER@@|$CLI_CONTAINER|g" \
-e "s|@@CLI_BASE_URL_OVERRIDE@@|$CLI_BASE_URL_OVERRIDE|g" \
"$SCRIPT_DIR/scripts/trigger-claude-refresh.sh" > "$TRIGGER_SCRIPT"
chmod +x "$TRIGGER_SCRIPT"
success "Installed $TRIGGER_SCRIPT"
# Install systemd service and timer
sed "s|@@TRIGGER_SCRIPT_PATH@@|$TRIGGER_SCRIPT|g" \
"$SCRIPT_DIR/templates/trigger-claude-refresh.service" > /etc/systemd/system/trigger-claude-refresh.service
cp "$SCRIPT_DIR/templates/trigger-claude-refresh.timer" /etc/systemd/system/
success "Installed systemd timer"
systemctl daemon-reload
systemctl enable trigger-claude-refresh.timer
systemctl start trigger-claude-refresh.timer
success "Auto-refresh trigger timer started (runs every 30 minutes)"
TRIGGER_INSTALLED=true
# Run the trigger script once to verify
info "Running trigger script for initial verification..."
bash "$TRIGGER_SCRIPT" 2>&1 | while read -r line; do echo " $line"; done
else
error "CLI test failed (exit code: $TEST_EXIT)"
if [ -n "$TEST_OUTPUT" ]; then
error "Output: $TEST_OUTPUT"
fi
warn "Skipping auto-refresh trigger installation"
warn "You can re-run the wizard later to retry"
fi
fi
# ============================================================================
# STEP 9: Initial Sync
# ============================================================================
header "Step 9: Running Initial Sync"
# Create oauth.json
mkdir -p "$(dirname "$OPENCLAW_OAUTH_FILE")"
python3 -c "
import json, time
with open('$CLAUDE_CREDS_FILE') as f:
src = json.load(f)
oauth = src['claudeAiOauth']
openclaw = {
'anthropic': {
'access': oauth['accessToken'],
'refresh': oauth['refreshToken'],
'expires': oauth['expiresAt'],
'scopes': oauth.get('scopes', []),
'subscriptionType': oauth.get('subscriptionType', 'max'),
'rateLimitTier': oauth.get('rateLimitTier', 'default_claude_max_5x')
}
}
with open('$OPENCLAW_OAUTH_FILE', 'w') as f:
json.dump(openclaw, f)
remaining = (oauth['expiresAt'] / 1000 - time.time()) / 3600
print(f'Token: {oauth[\"accessToken\"][:20]}... expires in {remaining:.1f}h')
"
success "Created $OPENCLAW_OAUTH_FILE"
# Update .env token
python3 -c "
import json, re
with open('$CLAUDE_CREDS_FILE') as f:
token = json.load(f)['claudeAiOauth']['accessToken']
with open('$ENV_FILE') as f:
env = f.read()
env = re.sub(r'ANTHROPIC_OAUTH_TOKEN=.*', f'ANTHROPIC_OAUTH_TOKEN=\"{token}\"', env)
with open('$ENV_FILE', 'w') as f:
f.write(env)
"
success "Updated $ENV_FILE"
# Recreate gateway
info "Recreating gateway container..."
cd "$COMPOSE_DIR"
docker compose down openclaw-gateway 2>&1 | tail -2
docker compose up -d openclaw-gateway 2>&1 | tail -2
success "Gateway recreated with fresh token"
# ============================================================================
# STEP 10: Verify
# ============================================================================
header "Step 10: Verification"
sleep 3
ERRORS=0
# Check service
if [ "$USE_INOTIFY" = true ]; then
if systemctl is-active --quiet sync-oauth-token.service; then
success "sync-oauth-token.service is running"
else
error "Service not running"
ERRORS=$((ERRORS + 1))
fi
else
if systemctl is-active --quiet refresh-claude-token.timer; then
success "refresh-claude-token.timer is active"
else
error "Timer not active"
ERRORS=$((ERRORS + 1))
fi
fi
# Check trigger timer
if [ "$TRIGGER_INSTALLED" = true ]; then
if systemctl is-active --quiet trigger-claude-refresh.timer; then
success "trigger-claude-refresh.timer is active"
else
error "Trigger timer not active"
ERRORS=$((ERRORS + 1))
fi
fi
# Check oauth.json
if [ -f "$OPENCLAW_OAUTH_FILE" ]; then
HAS_ACCESS=$(python3 -c "
import json
with open('$OPENCLAW_OAUTH_FILE') as f:
d = json.load(f)
print('yes' if d.get('anthropic', {}).get('access') else 'no')
" 2>/dev/null)
if [ "$HAS_ACCESS" = "yes" ]; then
success "oauth.json has correct format"
else
error "oauth.json missing anthropic.access"
ERRORS=$((ERRORS + 1))
fi
else
error "oauth.json not found"
ERRORS=$((ERRORS + 1))
fi
# Check gateway
if docker ps --format '{{.Names}}' 2>/dev/null | grep -q "$GATEWAY_CONTAINER"; then
success "Gateway container is running"
CTR_TOKEN=$(docker exec "$GATEWAY_CONTAINER" printenv ANTHROPIC_OAUTH_TOKEN 2>/dev/null | cut -c1-20)
if [ -n "$CTR_TOKEN" ]; then
success "Container has ANTHROPIC_OAUTH_TOKEN: ${CTR_TOKEN}..."
fi
else
error "Gateway container not running"
ERRORS=$((ERRORS + 1))
fi
# ============================================================================
# SUMMARY
# ============================================================================
echo ""
echo -e "${BOLD}${CYAN}"
echo " ╔══════════════════════════════════════════════════════╗"
if [ "$ERRORS" -eq 0 ]; then
echo " ║ Setup Complete! ║"
else
echo " ║ Setup Complete (with $ERRORS warning(s)) ║"
fi
echo " ╚══════════════════════════════════════════════════════╝"
echo -e "${NC}"
echo -e " ${BOLD}Installed:${NC}"
if [ "$USE_INOTIFY" = true ]; then
echo " - sync-oauth-token.sh (real-time file watcher)"
echo " - sync-oauth-token.service (systemd)"
else
echo " - refresh-claude-token.sh (direct API refresh)"
echo " - refresh-claude-token.timer (every 6 hours)"
fi
if [ "$TRIGGER_INSTALLED" = true ]; then
echo " - trigger-claude-refresh.sh (auto-refresh trigger)"
echo " - trigger-claude-refresh.timer (every 30 minutes)"
echo " - Claude CLI mode: $CLI_MODE$([ "$CLI_MODE" = "container" ] && echo " ($CLI_CONTAINER)")"
fi
echo " - Anthropic model configured in openclaw.json"
echo " - Auth profiles updated for all agents"
echo " - oauth.json created with fresh token"
echo ""
echo -e " ${BOLD}Useful commands:${NC}"
if [ "$USE_INOTIFY" = true ]; then
echo " journalctl -u sync-oauth-token.service -f # Watch sync logs"
echo " systemctl restart sync-oauth-token.service # Force re-sync"
else
echo " journalctl -u refresh-claude-token.service # View last refresh"
echo " systemctl list-timers refresh-claude-token* # Check timer"
fi
if [ "$TRIGGER_INSTALLED" = true ]; then
echo " journalctl -u trigger-claude-refresh -n 20 # Trigger logs"
echo " systemctl list-timers trigger-claude-refresh* # Check trigger timer"
fi
echo " ./scripts/verify.sh # Health check"
echo " ./setup.sh --uninstall # Remove everything"
echo ""