#!/usr/bin/env bash set -euo pipefail # Ensure Homebrew and brew-installed binaries are in PATH # This is needed for OpenClaw skills that depend on CLI tools (gemini, aider, etc.) export PATH="/home/linuxbrew/.linuxbrew/bin:/home/linuxbrew/.linuxbrew/sbin:${PATH}" # Home Assistant add-on options are usually rendered to /data/options.json OPTIONS_FILE="/data/options.json" if [ ! -f "$OPTIONS_FILE" ]; then echo "Missing $OPTIONS_FILE (add-on options)." exit 1 fi # ------------------------------------------------------------------------------ # Read add-on options (only add-on-specific knobs; OpenClaw is configured via onboarding) # ------------------------------------------------------------------------------ TZNAME=$(jq -r '.timezone // "Europe/Sofia"' "$OPTIONS_FILE") GW_PUBLIC_URL=$(jq -r '.gateway_public_url // empty' "$OPTIONS_FILE") HA_TOKEN=$(jq -r '.homeassistant_token // empty' "$OPTIONS_FILE") ENABLE_TERMINAL=$(jq -r '.enable_terminal // true' "$OPTIONS_FILE") TERMINAL_PORT=$(jq -r '.terminal_port // 7681' "$OPTIONS_FILE") echo "DEBUG: enable_terminal config value: '$ENABLE_TERMINAL'" echo "DEBUG: terminal_port config value: '$TERMINAL_PORT'" # Generic router SSH settings ROUTER_HOST=$(jq -r '.router_ssh_host // empty' "$OPTIONS_FILE") ROUTER_USER=$(jq -r '.router_ssh_user // empty' "$OPTIONS_FILE") ROUTER_KEY=$(jq -r '.router_ssh_key_path // "/data/keys/router_ssh"' "$OPTIONS_FILE") # Optional: allow disabling lock cleanup if you ever need to debug CLEAN_LOCKS_ON_START=$(jq -r '.clean_session_locks_on_start // true' "$OPTIONS_FILE") CLEAN_LOCKS_ON_EXIT=$(jq -r '.clean_session_locks_on_exit // true' "$OPTIONS_FILE") # Gateway bind mode (loopback or lan) GATEWAY_BIND_MODE=$(jq -r '.gateway_bind_mode // "loopback"' "$OPTIONS_FILE") GATEWAY_PORT=$(jq -r '.gateway_port // 18789' "$OPTIONS_FILE") ALLOW_INSECURE_AUTH=$(jq -r '.allow_insecure_auth // false' "$OPTIONS_FILE") export TZ="$TZNAME" # Reduce risk of secrets ending up in logs set +x # HA add-ons mount persistent storage at /config (maps to /addon_configs/ on the host). export HOME=/config # Explicitly set OpenClaw directories to ensure they persist across add-on updates # This prevents loss of installed skills, configuration, and workspace state export OPENCLAW_CONFIG_DIR=/config/.openclaw export OPENCLAW_WORKSPACE_DIR=/config/clawd export XDG_CONFIG_HOME=/config mkdir -p /config/.openclaw /config/clawd /config/keys /config/secrets # Back-compat: some docs/scripts assume /data; point it at /config. if [ ! -e /data ]; then ln -s /config /data || true fi # Ensure these exist so cleanup doesn't fail mkdir -p /config/.openclaw/agents/main/sessions || true # ------------------------------------------------------------------------------ # SINGLE-INSTANCE GUARD (prevents multiple gateway runs racing each other) # ------------------------------------------------------------------------------ STARTUP_LOCK="/config/.openclaw/gateway.start.lock" exec 9>"$STARTUP_LOCK" if ! flock -n 9; then echo "ERROR: Another instance appears to be running (could not acquire $STARTUP_LOCK)." echo "If this is wrong, check for stuck processes or remove the lock file." exit 1 fi # ------------------------------------------------------------------------------ # Session lock cleanup helpers # ------------------------------------------------------------------------------ gateway_running() { pgrep -f "openclaw.*gateway.*run" >/dev/null 2>&1 } cleanup_session_locks() { local sessions_dir="/config/.openclaw/agents/main/sessions" local glob1="${sessions_dir}"/*.jsonl.lock shopt -s nullglob local locks=( $glob1 ) shopt -u nullglob if [ ${#locks[@]} -eq 0 ]; then return 0 fi # If gateway is running, do NOT remove locks automatically (could be real). if gateway_running; then echo "INFO: Gateway appears to be running; leaving session lock files untouched." echo "INFO: Locks present: ${#locks[@]}" return 0 fi echo "INFO: Removing stale session lock files (${#locks[@]}) from ${sessions_dir}" rm -f "${sessions_dir}"/*.jsonl.lock || true } if [ "$CLEAN_LOCKS_ON_START" = "true" ]; then cleanup_session_locks else echo "INFO: clean_session_locks_on_start=false; skipping session lock cleanup." fi # ------------------------------------------------------------------------------ # Store tokens / export env vars (optional) # ------------------------------------------------------------------------------ if [ -n "$HA_TOKEN" ]; then umask 077 printf '%s' "$HA_TOKEN" > /config/secrets/homeassistant.token fi # ------------------------------------------------------------------------------ # OpenClaw config is managed by OpenClaw itself (onboarding / configure). # This add-on intentionally does NOT create/patch /config/.openclaw/openclaw.json. # ------------------------------------------------------------------------------ # Convenience info for later (router SSH access path & HA token file) cat > /config/CONNECTION_NOTES.txt </dev/null 2>&1; then kill -TERM "${NGINX_PID}" >/dev/null 2>&1 || true wait "${NGINX_PID}" || true fi if [ -n "${TTYD_PID}" ] && kill -0 "${TTYD_PID}" >/dev/null 2>&1; then kill -TERM "${TTYD_PID}" >/dev/null 2>&1 || true wait "${TTYD_PID}" || true fi if [ -n "${GW_PID}" ] && kill -0 "${GW_PID}" >/dev/null 2>&1; then kill -TERM "${GW_PID}" >/dev/null 2>&1 || true wait "${GW_PID}" || true fi if [ "$CLEAN_LOCKS_ON_EXIT" = "true" ]; then cleanup_session_locks || true fi } trap shutdown INT TERM if ! command -v openclaw >/dev/null 2>&1; then echo "ERROR: openclaw is not installed." exit 1 fi # Bootstrap minimal OpenClaw config ONLY if missing. # We do not overwrite or patch existing configs; onboarding owns everything else. OPENCLAW_CONFIG_PATH="/config/.openclaw/openclaw.json" if [ ! -f "$OPENCLAW_CONFIG_PATH" ]; then echo "INFO: OpenClaw config missing; bootstrapping minimal config at $OPENCLAW_CONFIG_PATH" python3 - <<'PY' import json import secrets from pathlib import Path cfg_path = Path('/config/.openclaw/openclaw.json') cfg_path.parent.mkdir(parents=True, exist_ok=True) cfg = { "gateway": { "mode": "local", "port": 18789, "bind": "loopback", "auth": { "mode": "token", "token": secrets.token_urlsafe(24) } }, "agents": { "defaults": { "workspace": "/config/clawd" } } } cfg_path.write_text(json.dumps(cfg, indent=2) + "\n", encoding='utf-8') print("INFO: Wrote minimal OpenClaw config (gateway.mode=local, auth.token generated)") PY fi # ------------------------------------------------------------------------------ # Apply gateway LAN mode settings safely using helper script # This updates gateway.bind and gateway.port without touching other settings # ------------------------------------------------------------------------------ export OPENCLAW_CONFIG_PATH="/config/.openclaw/openclaw.json" # Find the helper script (copied to root in Dockerfile, or fallback to add-on dir) HELPER_PATH="/oc_config_helper.py" if [ ! -f "$HELPER_PATH" ] && [ -f "$(dirname "$0")/oc_config_helper.py" ]; then HELPER_PATH="$(dirname "$0")/oc_config_helper.py" fi if [ -f "$OPENCLAW_CONFIG_PATH" ]; then if [ -f "$HELPER_PATH" ]; then if ! python3 "$HELPER_PATH" apply-gateway-settings "$GATEWAY_BIND_MODE" "$GATEWAY_PORT" "$ALLOW_INSECURE_AUTH"; then rc=$? echo "ERROR: Failed to apply gateway settings via oc_config_helper.py (exit code ${rc})." echo "ERROR: Gateway configuration may be incorrect; aborting startup." exit "${rc}" fi else echo "WARN: oc_config_helper.py not found, cannot apply gateway settings" echo "INFO: Ensure the add-on image includes oc_config_helper.py and restart" fi else echo "WARN: OpenClaw config not found at $OPENCLAW_CONFIG_PATH, cannot apply gateway settings" echo "INFO: Run 'openclaw onboard' first, then restart the add-on" fi echo "Starting OpenClaw Assistant gateway (openclaw)..." openclaw gateway run & GW_PID=$! # Start web terminal (optional) # Kill any stray ttyd processes from previous runs to avoid port conflicts pkill -f "ttyd.*-p.*-b /terminal" || true if [ "$ENABLE_TERMINAL" = "true" ] || [ "$ENABLE_TERMINAL" = "1" ]; then echo "Starting web terminal (ttyd) on 127.0.0.1:${TERMINAL_PORT} ..." ttyd -W -i 127.0.0.1 -p "${TERMINAL_PORT}" -b /terminal bash & TTYD_PID=$! else echo "Terminal disabled (enable_terminal=$ENABLE_TERMINAL)" fi # Start ingress reverse proxy (nginx). This provides the add-on UI inside HA. # Token is injected server-side; never put it in the browser URL. # Render nginx config from template. # The gateway token is NOT managed by the add-on; OpenClaw will generate/store it. # Best-effort: query it via CLI (works even if openclaw.json is JSON5). If unknown, we hide the button. GW_TOKEN="$(timeout 2s openclaw config get gateway.auth.token 2>/dev/null | tr -d '\n' || true)" GW_PUBLIC_URL="$GW_PUBLIC_URL" GW_TOKEN="$GW_TOKEN" TERMINAL_PORT="$TERMINAL_PORT" python3 - <<'PY' import os from pathlib import Path tpl = Path('/etc/nginx/nginx.conf.tpl').read_text() landing_tpl = Path('/etc/nginx/landing.html.tpl').read_text() public_url = os.environ.get('GW_PUBLIC_URL','') terminal_port = os.environ.get('TERMINAL_PORT', '7681') # Token comes from environment (best-effort CLI query in run.sh) token = os.environ.get('GW_TOKEN','') gw_path = '' if public_url.endswith('/') else '/' # Replace terminal port placeholder in nginx config conf = tpl.replace('__TERMINAL_PORT__', terminal_port) Path('/etc/nginx/nginx.conf').write_text(conf) landing = landing_tpl.replace('__GATEWAY_TOKEN__', token) landing = landing.replace('__GATEWAY_PUBLIC_URL__', public_url) landing = landing.replace('__GW_PUBLIC_URL_PATH__', gw_path) out_dir = Path('/etc/nginx/html') out_dir.mkdir(parents=True, exist_ok=True) out_file = out_dir / 'index.html' out_file.write_text(landing) # Ensure nginx can read it even if base image uses restrictive umask/permissions. try: out_dir.chmod(0o755) out_file.chmod(0o644) except Exception: pass PY echo "Starting ingress proxy (nginx) on :8099 ..." nginx -g 'daemon off;' & NGINX_PID=$! # Wait for gateway; if it exits, shut down others. wait "${GW_PID}"