entrypoint.sh creates /home/agent/vol/.openclaw-initializing at startup and removes it just before launching the gateway. Pod is not marked ready until init (volume copy + config baking) is fully complete. deployment.yaml adds readinessProbe: exec test ! -f sentinel, with initialDelaySeconds=5, periodSeconds=5, failureThreshold=60 (5 minute window for slow storage on first deploy).
540 lines
23 KiB
Bash
Executable File
540 lines
23 KiB
Bash
Executable File
#!/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; }
|
|
|
|
# ── Readiness sentinel ────────────────────────────────────────────
|
|
# Present during init; deleted just before gateway starts.
|
|
# Kubernetes readiness probe: test ! -f /home/agent/vol/.openclaw-initializing
|
|
SENTINEL="${VOL}/.openclaw-initializing"
|
|
mkdir -p "${VOL}"
|
|
touch "${SENTINEL}"
|
|
log "Readiness sentinel created at ${SENTINEL}"
|
|
|
|
# ── 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}..."
|
|
rm -f "${SENTINEL}"
|
|
log "Readiness sentinel removed — pod is ready."
|
|
/home/agent/.npm-global/bin/openclaw gateway &
|
|
|
|
while true; do
|
|
sleep 10
|
|
done
|