#!/bin/bash # OpenClaw + PAI Container Entrypoint # # Responsibilities: # 1. Warn on missing required env vars (but continue) # 2. Initialize or update OpenClaw state in the persistent volume # 3. Initialize or update PAI state in the persistent volume # 4. Start the OpenClaw gateway (manages its own restarts internally) # # Volume layout (all state lives under /home/agent/vol): # /home/agent/vol/.openclaw — OpenClaw state (OPENCLAW_STATE_DIR) # /home/agent/vol/.claude — PAI / Claude Code state (PAI_DIR, CLAUDE_CONFIG_DIR) # /home/agent/vol/.config/PAI — PAI config dir (PAI_CONFIG_DIR) # # Required env vars (warns if missing): # OPENCLAW_GATEWAY_TOKEN — gateway auth token # OPENCLAW_DEFAULT_MODEL — e.g. litellm/claude-sonnet-4-6 # OPENCLAW_AGENT_NAME — display name for the main OpenClaw agent # ANTHROPIC_API_KEY — Claude Code API key # ANTHROPIC_MODEL — PAI primary model # ANTHROPIC_SMALL_FAST_MODEL — PAI lightweight model # PAI_AGENT_NAME — display name for the PAI agent identity # # Optional env vars: # OPENCLAW_TLS — off (default) | auto | custom # OPENCLAW_TLS_CERT — base64-encoded cert PEM (required when OPENCLAW_TLS=custom) # OPENCLAW_TLS_KEY — base64-encoded key PEM (required when OPENCLAW_TLS=custom) # OPENCLAW_ALLOWED_ORIGINS — comma-separated extra allowed origins # OPENCLAW_DISABLE_DEVICE_AUTH — true | false (default false) # LITELLM_BASE_URL — enables litellm provider block in OpenClaw config # FORCE_OPENCLAW_REDEPLOY — true: wipe /home/agent/vol/.openclaw and redeploy from image # FORCE_PAI_REDEPLOY — true: wipe /home/agent/vol/.claude + .config/PAI and redeploy # ELEVENLABS_API_KEY — enables VoiceServer integration # # Native env vars (read directly by OpenClaw/Claude Code — no baking needed): # OPENCLAW_GATEWAY_PORT, OPENCLAW_GATEWAY_TOKEN, OPENCLAW_STATE_DIR # ANTHROPIC_API_KEY, OPENAI_API_KEY, GEMINI_API_KEY, ANTHROPIC_BASE_URL set -euo pipefail # ── Resolved paths ──────────────────────────────────────────────── VOL="/home/agent/vol" export OPENCLAW_STATE="${OPENCLAW_STATE_DIR:-${VOL}/.openclaw}" export PAI_STATE="${PAI_DIR:-${VOL}/.claude}" export PAI_CONFIG="${PAI_CONFIG_DIR:-${VOL}/.config/PAI}" export OPENCLAW_IMAGE="/home/agent/.openclaw.image" export PAI_IMAGE="/home/agent/.claude.image" export PAI_BASE_SETTINGS="${PAI_IMAGE}/settings.base.json" export OPENCLAW_BAKED_VARS="${OPENCLAW_STATE}/openclaw.baked-vars.json" export PAI_BAKED_VARS="${PAI_STATE}/settings.baked-vars.json" # CLAUDE_CONFIG_DIR must always match PAI_STATE export CLAUDE_CONFIG_DIR="${PAI_STATE}" # OPENCLAW_GATEWAY_PORT must be exported so resolveGatewayPort() picks it up on any restart export OPENCLAW_GATEWAY_PORT="${OPENCLAW_GATEWAY_PORT:-18800}" log() { echo "[entrypoint] $*"; } warn() { echo "[entrypoint] WARNING: $*" >&2; } # ── Warn on missing required vars ──────────────────────────────── for var in OPENCLAW_GATEWAY_TOKEN OPENCLAW_DEFAULT_MODEL OPENCLAW_AGENT_NAME \ ANTHROPIC_API_KEY ANTHROPIC_MODEL ANTHROPIC_SMALL_FAST_MODEL PAI_AGENT_NAME; do if [ -z "${!var:-}" ]; then warn "${var} is not set. The agent may not function correctly." fi done # ── OpenClaw state ──────────────────────────────────────────────── _oc_bake_config() { # Writes openclaw.json and openclaw.baked-vars.json from current env. # Safe to call on first deploy or when baked-vars are missing. python3 - <<'PYEOF' import json, os, sys config_path = os.environ['OPENCLAW_STATE'] + '/openclaw.json' baked_path = os.environ['OPENCLAW_BAKED_VARS'] with open(config_path) as f: config = json.load(f) def setpath(obj, keys, val): for k in keys[:-1]: obj = obj.setdefault(k, {}) obj[keys[-1]] = val def getenv(key, default=''): return os.environ.get(key, default) # Core baked fields model = getenv('OPENCLAW_DEFAULT_MODEL') if model: setpath(config, ['agents','defaults','model','primary'], model) workspace = getenv('OPENCLAW_WORKSPACE') or (os.environ['OPENCLAW_STATE'] + '/workspace') setpath(config, ['agents','defaults','workspace'], workspace) bind = getenv('OPENCLAW_GATEWAY_BIND') or 'lan' setpath(config, ['gateway','bind'], bind) agent_name = getenv('OPENCLAW_AGENT_NAME') if agent_name: agents_list = config.setdefault('agents', {}).setdefault('list', []) main_agent = next((a for a in agents_list if a.get('id') == 'main'), None) if main_agent is None: main_agent = {'id': 'main', 'default': True} agents_list.insert(0, main_agent) main_agent.setdefault('identity', {})['name'] = agent_name # LiteLLM provider — only add if LITELLM_BASE_URL is set litellm_url = getenv('LITELLM_BASE_URL') if litellm_url: setpath(config, ['models','providers','litellm','baseUrl'], litellm_url) setpath(config, ['models','providers','litellm','api'], 'openai-completions') if not config.get('models',{}).get('providers',{}).get('litellm',{}).get('models'): setpath(config, ['models','providers','litellm','models'], []) api_key = getenv('ANTHROPIC_API_KEY') if api_key: setpath(config, ['models','providers','litellm','apiKey'], api_key) # TLS tls_mode = getenv('OPENCLAW_TLS', 'off').lower().strip() port = getenv('OPENCLAW_GATEWAY_PORT', '18800') scheme = 'https' if tls_mode in ('auto', 'custom') else 'http' if tls_mode == 'off': # Remove any previously baked TLS config config.get('gateway', {}).pop('tls', None) elif tls_mode == 'auto': setpath(config, ['gateway','tls','enabled'], True) setpath(config, ['gateway','tls','autoGenerate'], True) # Remove custom cert paths if previously set config['gateway']['tls'].pop('certPath', None) config['gateway']['tls'].pop('keyPath', None) elif tls_mode == 'custom': cert_b64 = getenv('OPENCLAW_TLS_CERT') key_b64 = getenv('OPENCLAW_TLS_KEY') if not cert_b64 or not key_b64: print('[entrypoint] WARNING: OPENCLAW_TLS=custom but OPENCLAW_TLS_CERT or OPENCLAW_TLS_KEY is missing. Falling back to TLS off.') config.get('gateway', {}).pop('tls', None) scheme = 'http' else: import base64, pathlib try: cert_bytes = base64.b64decode(cert_b64, validate=True) key_bytes = base64.b64decode(key_b64, validate=True) except Exception as e: print(f'[entrypoint] WARNING: OPENCLAW_TLS=custom but cert/key base64 decode failed: {e}. Falling back to TLS off.') config.get('gateway', {}).pop('tls', None) scheme = 'http' else: tls_dir = pathlib.Path(os.environ['OPENCLAW_STATE']) / 'tls' tls_dir.mkdir(mode=0o700, parents=True, exist_ok=True) cert_path = tls_dir / 'cert.pem' key_path = tls_dir / 'key.pem' cert_path.write_bytes(cert_bytes) key_path.write_bytes(key_bytes) cert_path.chmod(0o600) key_path.chmod(0o600) setpath(config, ['gateway','tls','enabled'], True) setpath(config, ['gateway','tls','certPath'], str(cert_path)) setpath(config, ['gateway','tls','keyPath'], str(key_path)) config['gateway']['tls'].pop('autoGenerate', None) print('[entrypoint] TLS cert and key written.') else: print(f'[entrypoint] WARNING: Unknown OPENCLAW_TLS value "{tls_mode}". Falling back to TLS off.') config.get('gateway', {}).pop('tls', None) scheme = 'http' # controlUi origins — localhost + 127.0.0.1 always; user-provided extras optional origins = [ f'{scheme}://localhost:{port}', f'{scheme}://127.0.0.1:{port}', ] extra = getenv('OPENCLAW_ALLOWED_ORIGINS') if extra: origins.extend([o.strip() for o in extra.split(',') if o.strip()]) setpath(config, ['gateway','controlUi','allowedOrigins'], origins) # dangerouslyDisableDeviceAuth if getenv('OPENCLAW_DISABLE_DEVICE_AUTH', 'false').lower() == 'true': setpath(config, ['gateway','controlUi','dangerouslyDisableDeviceAuth'], True) else: # Remove if previously set config.get('gateway', {}).get('controlUi', {}).pop('dangerouslyDisableDeviceAuth', None) # Memory search memory_provider = getenv('OPENCLAW_MEMORY_SEARCH_PROVIDER').lower().strip() gemini_key = getenv('GEMINI_API_KEY') if memory_provider == 'gemini' and gemini_key: setpath(config, ['agents','defaults','memorySearch','enabled'], True) setpath(config, ['agents','defaults','memorySearch','provider'], 'gemini') setpath(config, ['agents','defaults','memorySearch','remote','apiKey'], gemini_key) elif memory_provider and memory_provider != 'gemini': print(f'[entrypoint] WARNING: OPENCLAW_MEMORY_SEARCH_PROVIDER={memory_provider} is not yet supported by the entrypoint. Skipping.') elif memory_provider == 'gemini' and not gemini_key: print('[entrypoint] WARNING: OPENCLAW_MEMORY_SEARCH_PROVIDER=gemini but GEMINI_API_KEY is not set. Skipping.') # Never write gateway.port — env var controls it config.get('gateway', {}).pop('port', None) with open(config_path, 'w') as f: json.dump(config, f, indent=2) baked = { 'OPENCLAW_DEFAULT_MODEL': getenv('OPENCLAW_DEFAULT_MODEL'), 'OPENCLAW_WORKSPACE': workspace, 'OPENCLAW_GATEWAY_BIND': bind, 'OPENCLAW_AGENT_NAME': getenv('OPENCLAW_AGENT_NAME'), 'OPENCLAW_GATEWAY_PORT': port, 'LITELLM_BASE_URL': getenv('LITELLM_BASE_URL'), 'ANTHROPIC_API_KEY': getenv('ANTHROPIC_API_KEY'), 'OPENCLAW_TLS': tls_mode, 'OPENCLAW_ALLOWED_ORIGINS': getenv('OPENCLAW_ALLOWED_ORIGINS'), 'OPENCLAW_DISABLE_DEVICE_AUTH': getenv('OPENCLAW_DISABLE_DEVICE_AUTH', 'false'), 'OPENCLAW_MEMORY_SEARCH_PROVIDER': memory_provider, 'GEMINI_API_KEY': gemini_key, } with open(baked_path, 'w') as f: json.dump(baked, f, indent=2) print('[entrypoint] OpenClaw config baked.') PYEOF } _oc_update_config() { # Surgically update only fields whose env vars have changed since last bake. python3 - <<'PYEOF' import json, os, sys config_path = os.environ['OPENCLAW_STATE'] + '/openclaw.json' baked_path = os.environ['OPENCLAW_BAKED_VARS'] def getenv(key, default=''): return os.environ.get(key, default) workspace = getenv('OPENCLAW_WORKSPACE') or (os.environ['OPENCLAW_STATE'] + '/workspace') bind = getenv('OPENCLAW_GATEWAY_BIND') or 'lan' port = getenv('OPENCLAW_GATEWAY_PORT', '18800') tls_mode = getenv('OPENCLAW_TLS', 'off').lower().strip() current = { 'OPENCLAW_DEFAULT_MODEL': getenv('OPENCLAW_DEFAULT_MODEL'), 'OPENCLAW_WORKSPACE': workspace, 'OPENCLAW_GATEWAY_BIND': bind, 'OPENCLAW_AGENT_NAME': getenv('OPENCLAW_AGENT_NAME'), 'OPENCLAW_GATEWAY_PORT': port, 'LITELLM_BASE_URL': getenv('LITELLM_BASE_URL'), 'ANTHROPIC_API_KEY': getenv('ANTHROPIC_API_KEY'), 'OPENCLAW_TLS': tls_mode, 'OPENCLAW_ALLOWED_ORIGINS': getenv('OPENCLAW_ALLOWED_ORIGINS'), 'OPENCLAW_DISABLE_DEVICE_AUTH': getenv('OPENCLAW_DISABLE_DEVICE_AUTH', 'false'), 'OPENCLAW_MEMORY_SEARCH_PROVIDER': getenv('OPENCLAW_MEMORY_SEARCH_PROVIDER').lower().strip(), 'GEMINI_API_KEY': getenv('GEMINI_API_KEY'), } with open(baked_path) as f: baked = json.load(f) changed = {k: current[k] for k in current if baked.get(k) != current[k]} if not changed: print('[entrypoint] OpenClaw config unchanged.') sys.exit(0) print(f'[entrypoint] OpenClaw config changes detected: {list(changed.keys())}') with open(config_path) as f: config = json.load(f) def setpath(obj, keys, val): for k in keys[:-1]: obj = obj.setdefault(k, {}) obj[keys[-1]] = val if 'OPENCLAW_DEFAULT_MODEL' in changed and changed['OPENCLAW_DEFAULT_MODEL']: setpath(config, ['agents','defaults','model','primary'], changed['OPENCLAW_DEFAULT_MODEL']) if 'OPENCLAW_WORKSPACE' in changed: setpath(config, ['agents','defaults','workspace'], changed['OPENCLAW_WORKSPACE']) if 'OPENCLAW_GATEWAY_BIND' in changed: setpath(config, ['gateway','bind'], changed['OPENCLAW_GATEWAY_BIND']) if 'OPENCLAW_AGENT_NAME' in changed and changed['OPENCLAW_AGENT_NAME']: agents_list = config.setdefault('agents', {}).setdefault('list', []) main_agent = next((a for a in agents_list if a.get('id') == 'main'), None) if main_agent is None: main_agent = {'id': 'main', 'default': True} agents_list.insert(0, main_agent) main_agent.setdefault('identity', {})['name'] = changed['OPENCLAW_AGENT_NAME'] if 'LITELLM_BASE_URL' in changed: if changed['LITELLM_BASE_URL']: setpath(config, ['models','providers','litellm','baseUrl'], changed['LITELLM_BASE_URL']) setpath(config, ['models','providers','litellm','api'], 'openai-completions') if not config.get('models',{}).get('providers',{}).get('litellm',{}).get('models'): setpath(config, ['models','providers','litellm','models'], []) else: config.get('models', {}).get('providers', {}).pop('litellm', None) if 'ANTHROPIC_API_KEY' in changed: if current.get('LITELLM_BASE_URL') and changed['ANTHROPIC_API_KEY']: setpath(config, ['models','providers','litellm','apiKey'], changed['ANTHROPIC_API_KEY']) # Recalculate scheme from current (not changed) TLS mode scheme = 'https' if tls_mode in ('auto', 'custom') else 'http' if 'OPENCLAW_TLS' in changed or 'OPENCLAW_GATEWAY_PORT' in changed: new_tls = tls_mode if new_tls == 'off': config.get('gateway', {}).pop('tls', None) elif new_tls == 'auto': setpath(config, ['gateway','tls','enabled'], True) setpath(config, ['gateway','tls','autoGenerate'], True) config['gateway']['tls'].pop('certPath', None) config['gateway']['tls'].pop('keyPath', None) elif new_tls == 'custom': cert_b64 = getenv('OPENCLAW_TLS_CERT') key_b64 = getenv('OPENCLAW_TLS_KEY') if not cert_b64 or not key_b64: print('[entrypoint] WARNING: OPENCLAW_TLS=custom but cert/key missing. Falling back to TLS off.') config.get('gateway', {}).pop('tls', None) scheme = 'http' else: import base64, pathlib try: cert_bytes = base64.b64decode(cert_b64, validate=True) key_bytes = base64.b64decode(key_b64, validate=True) except Exception as e: print(f'[entrypoint] WARNING: OPENCLAW_TLS=custom but cert/key base64 decode failed: {e}. Falling back to TLS off.') config.get('gateway', {}).pop('tls', None) scheme = 'http' else: tls_dir = pathlib.Path(os.environ['OPENCLAW_STATE']) / 'tls' tls_dir.mkdir(mode=0o700, parents=True, exist_ok=True) cert_path = tls_dir / 'cert.pem' key_path = tls_dir / 'key.pem' cert_path.write_bytes(cert_bytes) key_path.write_bytes(key_bytes) cert_path.chmod(0o600) key_path.chmod(0o600) setpath(config, ['gateway','tls','enabled'], True) setpath(config, ['gateway','tls','certPath'], str(cert_path)) setpath(config, ['gateway','tls','keyPath'], str(key_path)) config['gateway']['tls'].pop('autoGenerate', None) print('[entrypoint] TLS cert and key updated.') if 'OPENCLAW_TLS' in changed or 'OPENCLAW_GATEWAY_PORT' in changed or 'OPENCLAW_ALLOWED_ORIGINS' in changed: origins = [ f'{scheme}://localhost:{port}', f'{scheme}://127.0.0.1:{port}', ] extra = current.get('OPENCLAW_ALLOWED_ORIGINS', '') if extra: origins.extend([o.strip() for o in extra.split(',') if o.strip()]) setpath(config, ['gateway','controlUi','allowedOrigins'], origins) if 'OPENCLAW_DISABLE_DEVICE_AUTH' in changed: if changed['OPENCLAW_DISABLE_DEVICE_AUTH'].lower() == 'true': setpath(config, ['gateway','controlUi','dangerouslyDisableDeviceAuth'], True) else: config.get('gateway', {}).get('controlUi', {}).pop('dangerouslyDisableDeviceAuth', None) if 'OPENCLAW_MEMORY_SEARCH_PROVIDER' in changed or 'GEMINI_API_KEY' in changed: memory_provider = current.get('OPENCLAW_MEMORY_SEARCH_PROVIDER', '') gemini_key = current.get('GEMINI_API_KEY', '') if memory_provider == 'gemini' and gemini_key: setpath(config, ['agents','defaults','memorySearch','enabled'], True) setpath(config, ['agents','defaults','memorySearch','provider'], 'gemini') setpath(config, ['agents','defaults','memorySearch','remote','apiKey'], gemini_key) elif memory_provider == 'gemini' and not gemini_key: print('[entrypoint] WARNING: OPENCLAW_MEMORY_SEARCH_PROVIDER=gemini but GEMINI_API_KEY is not set. Skipping.') elif not memory_provider: # Provider removed — clear memorySearch block config.get('agents', {}).get('defaults', {}).pop('memorySearch', None) config.get('gateway', {}).pop('port', None) with open(config_path, 'w') as f: json.dump(config, f, indent=2) with open(baked_path, 'w') as f: json.dump(current, f, indent=2) print('[entrypoint] OpenClaw config updated.') PYEOF } if [ "${FORCE_OPENCLAW_REDEPLOY:-false}" = "true" ]; then log "FORCE_OPENCLAW_REDEPLOY=true: wiping ${OPENCLAW_STATE} and redeploying from image." rm -rf "${OPENCLAW_STATE}" fi if [ ! -d "${OPENCLAW_STATE}" ]; then log "First deploy: initializing OpenClaw state at ${OPENCLAW_STATE}" mkdir -p "${OPENCLAW_STATE}" cp -r "${OPENCLAW_IMAGE}/." "${OPENCLAW_STATE}/" _oc_bake_config # Seed IDENTITY.md with agent name if provided if [ -n "${OPENCLAW_AGENT_NAME:-}" ]; then IDENTITY_PATH="${OPENCLAW_STATE}/workspace/IDENTITY.md" if [ ! -f "${IDENTITY_PATH}" ]; then mkdir -p "${OPENCLAW_STATE}/workspace" printf '# IDENTITY.md - Who Am I?\n\n- **Name:** %s\n' "${OPENCLAW_AGENT_NAME}" > "${IDENTITY_PATH}" log "IDENTITY.md seeded with name: ${OPENCLAW_AGENT_NAME}" fi fi log "OpenClaw state initialized." elif [ ! -f "${OPENCLAW_BAKED_VARS}" ]; then log "OpenClaw directory exists but baked-vars missing — baking config." _oc_bake_config else _oc_update_config fi # Clear TLS secrets from environment after use unset OPENCLAW_TLS_CERT OPENCLAW_TLS_KEY 2>/dev/null || true # ── PAI state ───────────────────────────────────────────────────── _pai_bake_config() { mkdir -p "${PAI_CONFIG}" log "Baking PAI configuration..." envsubst '${PAI_DIR} ${PAI_CONFIG_DIR} ${PROJECTS_DIR} ${ANTHROPIC_BASE_URL} ${ANTHROPIC_API_KEY} ${ANTHROPIC_MODEL} ${ANTHROPIC_SMALL_FAST_MODEL} ${VOICE_SERVER_URL} ${PAI_AGENT_NAME}' \ < "${PAI_BASE_SETTINGS}" \ > "${PAI_STATE}/settings.json" python3 - <<'PYEOF' import json, os baked = { 'PAI_DIR': os.environ.get('PAI_DIR', ''), 'PAI_CONFIG_DIR': os.environ.get('PAI_CONFIG_DIR', ''), 'PROJECTS_DIR': os.environ.get('PROJECTS_DIR', ''), 'ANTHROPIC_BASE_URL': os.environ.get('ANTHROPIC_BASE_URL', ''), 'ANTHROPIC_API_KEY': os.environ.get('ANTHROPIC_API_KEY', ''), 'ANTHROPIC_MODEL': os.environ.get('ANTHROPIC_MODEL', ''), 'ANTHROPIC_SMALL_FAST_MODEL': os.environ.get('ANTHROPIC_SMALL_FAST_MODEL', ''), 'VOICE_SERVER_URL': os.environ.get('VOICE_SERVER_URL', ''), 'PAI_AGENT_NAME': os.environ.get('PAI_AGENT_NAME', ''), } with open(os.environ['PAI_BAKED_VARS'], 'w') as f: json.dump(baked, f, indent=2) print('[entrypoint] PAI config baked.') PYEOF } _pai_update_config() { python3 - <<'PYEOF' import json, os, sys settings_path = os.environ['PAI_STATE'] + '/settings.json' baked_path = os.environ['PAI_BAKED_VARS'] current = { 'PAI_DIR': os.environ.get('PAI_DIR', ''), 'PAI_CONFIG_DIR': os.environ.get('PAI_CONFIG_DIR', ''), 'PROJECTS_DIR': os.environ.get('PROJECTS_DIR', ''), 'ANTHROPIC_BASE_URL': os.environ.get('ANTHROPIC_BASE_URL', ''), 'ANTHROPIC_API_KEY': os.environ.get('ANTHROPIC_API_KEY', ''), 'ANTHROPIC_MODEL': os.environ.get('ANTHROPIC_MODEL', ''), 'ANTHROPIC_SMALL_FAST_MODEL': os.environ.get('ANTHROPIC_SMALL_FAST_MODEL', ''), 'VOICE_SERVER_URL': os.environ.get('VOICE_SERVER_URL', ''), 'PAI_AGENT_NAME': os.environ.get('PAI_AGENT_NAME', ''), } with open(baked_path) as f: baked = json.load(f) changed = {k: v for k, v in current.items() if baked.get(k) != v} if not changed: print('[entrypoint] PAI config unchanged.') sys.exit(0) print(f'[entrypoint] PAI config changes detected: {list(changed.keys())}') with open(settings_path) as f: content = f.read() for key, new_val in changed.items(): old_val = baked.get(key, '') if old_val and old_val in content: content = content.replace(old_val, new_val) content = content.replace(f'${{{key}}}', new_val) with open(settings_path, 'w') as f: f.write(content) with open(baked_path, 'w') as f: json.dump(current, f, indent=2) print('[entrypoint] PAI config updated.') PYEOF } if [ "${FORCE_PAI_REDEPLOY:-false}" = "true" ]; then log "FORCE_PAI_REDEPLOY=true: wiping ${PAI_STATE} and ${PAI_CONFIG} and redeploying from image." rm -rf "${PAI_STATE}" "${PAI_CONFIG}" fi if [ ! -d "${PAI_STATE}" ]; then log "First deploy: initializing PAI state at ${PAI_STATE}" mkdir -p "${PAI_STATE}" cp -r "${PAI_IMAGE}/." "${PAI_STATE}/" _pai_bake_config log "PAI state initialized." elif [ ! -f "${PAI_BAKED_VARS}" ]; then log "PAI directory exists but baked-vars missing — baking config." _pai_bake_config else _pai_update_config fi # ── Optional integrations ───────────────────────────────────────── [ -z "${ELEVENLABS_API_KEY:-}" ] && log "ELEVENLABS_API_KEY not set — VoiceServer disabled." # ── Start OpenClaw gateway ──────────────────────────────────────── # Run gateway in background. OpenClaw manages its own internal restarts. # The sleep loop keeps the container alive if the gateway process exits. log "Starting OpenClaw gateway on port ${OPENCLAW_GATEWAY_PORT}..." /home/agent/.npm-global/bin/openclaw gateway & while true; do sleep 10 done