diff --git a/.github/workflows/release-discord.yaml b/.github/workflows/release-discord.yaml
index de852ba..55d1799 100644
--- a/.github/workflows/release-discord.yaml
+++ b/.github/workflows/release-discord.yaml
@@ -41,7 +41,7 @@ jobs:
fi
PAYLOAD=$(jq -n \
- --arg title "🚀 New release published: $TITLE" \
+ --arg title "🚀 New App/Add-on release published: $TITLE" \
--arg repo "$REPO" \
--arg tag "$RELEASE_TAG" \
--arg url "$RELEASE_URL" \
diff --git a/openclaw_assistant/CHANGELOG.md b/openclaw_assistant/CHANGELOG.md
index dbf03f1..66280d7 100644
--- a/openclaw_assistant/CHANGELOG.md
+++ b/openclaw_assistant/CHANGELOG.md
@@ -2,6 +2,26 @@
All notable changes to the OpenClaw Assistant Home Assistant Add-on will be documented in this file.
+## [0.5.54] - 2026-02-25
+
+### Changed
+- Added startup guidance when `gateway_auth_mode=trusted-proxy` is enabled to clarify why direct local CLI gateway calls can show `trusted_proxy_user_missing`/unauthorized.
+- Bump OpenClaw to 2026.2.24.
+
+### Added
+- New add-on option `gateway_additional_allowed_origins` for extra Control UI origins in `lan_https` mode.
+- **Custom SANs in TLS certificate** (`lan_https` mode): hostnames and IPs from `gateway_additional_allowed_origins` and `gateway_public_url` are now included in the server certificate's Subject Alternative Name. The certificate auto-regenerates when SANs change.
+
+### Fixed
+- **Gateway token on landing page**: read token directly from `openclaw.json` instead of via `openclaw config get` which redacts secrets since OpenClaw v2026.2.22+ (fixes "Open Gateway Web UI" button sending `openclaw_redacted` as the token).
+- **Token retrieval instructions**: all "get your token" references in the landing page and DOCS now use `jq -r '.gateway.auth.token' /config/.openclaw/openclaw.json` with a note explaining why the old `openclaw config get` command no longer works.
+- `lan_https` startup no longer overwrites `gateway.controlUi.allowedOrigins` with defaults only.
+- Control UI origins are now merged as: built-in defaults + existing config values + `gateway_additional_allowed_origins` (deduplicated).
+- In `lan_reverse_proxy` and other non-`lan_https` setups, Control UI origins now also include the origin derived from `gateway_public_url`.
+- `gateway.controlUi.allowedOrigins` configuration is now consistently applied via merge logic (defaults + existing values + user extras), reducing manual `openclaw.json` edits after upgrades.
+- Add-on no longer exits/restarts when OpenClaw runtime process is restarted during onboarding or config changes.
+- `run.sh` now supervises the OpenClaw runtime (`openclaw gateway run` / `openclaw node run`) and auto-restarts it while keeping nginx + terminal alive.
+
## [0.5.53] - 2026-02-24
- Bump OpenClaw to 2026.2.23.
diff --git a/openclaw_assistant/Dockerfile b/openclaw_assistant/Dockerfile
index ad2a059..2068dab 100644
--- a/openclaw_assistant/Dockerfile
+++ b/openclaw_assistant/Dockerfile
@@ -103,7 +103,7 @@ USER root
# Install OpenClaw globally
RUN npm config set fund false && npm config set audit false \
- && npm install -g openclaw@2026.2.23
+ && npm install -g openclaw@2026.2.24
# Shell aliases and color options for interactive use
RUN tee -a /etc/bash.bashrc <<'EOF'
diff --git a/openclaw_assistant/config.yaml b/openclaw_assistant/config.yaml
index cd61183..99992f5 100644
--- a/openclaw_assistant/config.yaml
+++ b/openclaw_assistant/config.yaml
@@ -1,5 +1,5 @@
name: OpenClaw Assistant
-version: "0.5.53"
+version: "0.5.54"
slug: openclaw_assistant
description: Run OpenClaw Assistant (OpenClaw-compatible) as a Home Assistant add-on.
url: https://github.com/techartdev/OpenClawHomeAssistant
@@ -62,6 +62,10 @@ options:
# Default is local.
gateway_mode: local
+ # Remote gateway URL (used when gateway_mode=remote)
+ # Example: "ws://192.168.1.20:18789" or "wss://gateway.example.com:443"
+ gateway_remote_url: ""
+
# Gateway network bind mode:
# - loopback: bind to 127.0.0.1 only (local access only, most secure)
# - lan: bind to all interfaces (accessible from local network)
@@ -89,6 +93,13 @@ options:
# Example: "127.0.0.1,192.168.88.0/24"
gateway_trusted_proxies: ""
+ # Additional allowed origins for gateway.controlUi.allowedOrigins.
+ # Merged with built-in defaults and existing configured origins.
+ # In lan_https mode, hostnames/IPs from these origins are also added to the
+ # TLS certificate SAN so the cert is valid for custom domains.
+ # Example: "https://ha.example.com:8443,capacitor://localhost"
+ gateway_additional_allowed_origins: ""
+
# Enable OpenAI-compatible Chat Completions API endpoint
# When enabled, OpenClaw can be used as a conversation agent in HA Assist pipeline
# via Extended OpenAI Conversation (HACS) or any OpenAI-compatible client
@@ -133,11 +144,13 @@ schema:
clean_session_locks_on_start: bool?
clean_session_locks_on_exit: bool?
gateway_mode: list(local|remote)?
+ gateway_remote_url: str?
gateway_bind_mode: list(loopback|lan|tailnet)?
gateway_port: int(1,65535)?
access_mode: list(custom|local_only|lan_https|lan_reverse_proxy|tailnet_https)?
gateway_auth_mode: list(token|trusted-proxy)?
gateway_trusted_proxies: str?
+ gateway_additional_allowed_origins: str?
enable_openai_api: bool?
force_ipv4_dns: bool?
gateway_env_vars:
diff --git a/openclaw_assistant/icon.png b/openclaw_assistant/icon.png
index 4e37bbf..1d95d95 100644
Binary files a/openclaw_assistant/icon.png and b/openclaw_assistant/icon.png differ
diff --git a/openclaw_assistant/landing.html.tpl b/openclaw_assistant/landing.html.tpl
index 5ee3357..f04a4e5 100644
--- a/openclaw_assistant/landing.html.tpl
+++ b/openclaw_assistant/landing.html.tpl
@@ -109,8 +109,9 @@
Set gateway_public_url in add-on options if the button URL is wrong.
- If the Gateway UI says Unauthorized, get your token in the terminal:
- openclaw config get gateway.auth.token
+ If the Gateway UI says Unauthorized, get your token from the terminal:
+ jq -r '.gateway.auth.token' /config/.openclaw/openclaw.json
+ (Since OpenClaw v2026.2.22+, openclaw config get redacts secrets — read the file directly instead.)
diff --git a/openclaw_assistant/logo.png b/openclaw_assistant/logo.png
index 4e37bbf..1d95d95 100644
Binary files a/openclaw_assistant/logo.png and b/openclaw_assistant/logo.png differ
diff --git a/openclaw_assistant/oc_config_helper.py b/openclaw_assistant/oc_config_helper.py
index 69e557d..5aea55f 100644
--- a/openclaw_assistant/oc_config_helper.py
+++ b/openclaw_assistant/oc_config_helper.py
@@ -57,12 +57,13 @@ def set_gateway_setting(key, value):
return write_config(cfg)
-def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_api: bool, auth_mode: str, trusted_proxies_csv: str):
+def apply_gateway_settings(mode: str, remote_url: str, bind_mode: str, port: int, enable_openai_api: bool, auth_mode: str, trusted_proxies_csv: str):
"""
Apply gateway settings to OpenClaw config.
Args:
mode: "local" or "remote"
+ remote_url: remote Gateway websocket URL (used when mode=remote)
bind_mode: "loopback", "lan", or "tailnet"
port: Port number to listen on (must be 1-65535)
enable_openai_api: Enable OpenAI-compatible Chat Completions endpoint
@@ -97,7 +98,12 @@ def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_a
cfg["gateway"] = {}
gateway = cfg["gateway"]
-
+
+ # gateway.remote settings
+ if "remote" not in gateway or not isinstance(gateway.get("remote"), dict):
+ gateway["remote"] = {}
+ remote_cfg = gateway["remote"]
+
# auth should be nested inside gateway
if "auth" not in gateway:
gateway["auth"] = {}
@@ -120,6 +126,7 @@ def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_a
trusted_proxy_cfg_default = {"userHeader": "x-forwarded-user"}
current_mode = gateway.get("mode", "")
+ current_remote_url = remote_cfg.get("url", "")
current_bind = gateway.get("bind", "")
current_port = gateway.get("port", 18789)
current_openai_api = chat_completions.get("enabled", False)
@@ -132,6 +139,10 @@ def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_a
if current_mode != mode:
gateway["mode"] = mode
changes.append(f"mode: {current_mode} -> {mode}")
+
+ if current_remote_url != remote_url:
+ remote_cfg["url"] = remote_url
+ changes.append(f"remote.url: {current_remote_url} -> {remote_url}")
if current_bind != bind_mode:
gateway["bind"] = bind_mode
@@ -166,11 +177,11 @@ def apply_gateway_settings(mode: str, bind_mode: str, port: int, enable_openai_a
print("ERROR: Failed to write config")
return False
else:
- print(f"INFO: Gateway settings already correct (mode={mode}, bind={bind_mode}, port={port}, chatCompletions={enable_openai_api}, authMode={auth_mode}, trustedProxies={trusted_proxies})")
+ print(f"INFO: Gateway settings already correct (mode={mode}, remoteUrl={remote_url}, bind={bind_mode}, port={port}, chatCompletions={enable_openai_api}, authMode={auth_mode}, trustedProxies={trusted_proxies})")
return True
-def set_control_ui_origins(origins_csv: str):
+def set_control_ui_origins(origins_csv: str, additional_origins_csv: str = ""):
"""
Configure gateway.controlUi for the built-in HTTPS proxy.
@@ -186,8 +197,8 @@ def set_control_ui_origins(origins_csv: str):
been written by earlier add-on versions.
Args:
- origins_csv: Comma-separated list of allowed origins.
- Pass an empty string to clear / leave unset.
+ origins_csv: Comma-separated list of default origins provided by the add-on.
+ additional_origins_csv: Comma-separated list of user-provided extra origins.
"""
cfg = read_config()
if cfg is None:
@@ -201,14 +212,23 @@ def set_control_ui_origins(origins_csv: str):
gateway["controlUi"] = {}
control_ui = gateway["controlUi"]
- origins = [o.strip() for o in origins_csv.split(",") if o.strip()]
+ default_origins = [o.strip() for o in origins_csv.split(",") if o.strip()]
+ additional_origins = [o.strip() for o in (additional_origins_csv or "").split(",") if o.strip()]
changes = []
# --- allowedOrigins ---
current_origins = control_ui.get("allowedOrigins", [])
- if current_origins != origins:
- control_ui["allowedOrigins"] = origins
- changes.append(f"allowedOrigins: {current_origins} -> {origins}")
+ if not isinstance(current_origins, list):
+ current_origins = []
+
+ merged_origins = []
+ for origin in [*default_origins, *current_origins, *additional_origins]:
+ if isinstance(origin, str) and origin and origin not in merged_origins:
+ merged_origins.append(origin)
+
+ if current_origins != merged_origins:
+ control_ui["allowedOrigins"] = merged_origins
+ changes.append(f"allowedOrigins: {current_origins} -> {merged_origins}")
# --- dangerouslyDisableDeviceAuth ---
# Skips the interactive pairing handshake (error 1008: pairing required).
@@ -224,7 +244,7 @@ def set_control_ui_origins(origins_csv: str):
changes.append(f"removed invalid key: {stale_key}")
if not changes:
- print(f"INFO: controlUi already correct: origins={origins}, deviceAuth=disabled")
+ print(f"INFO: controlUi already correct: origins={merged_origins}, deviceAuth=disabled")
return True
if write_config(cfg):
@@ -243,16 +263,17 @@ def main():
cmd = sys.argv[1]
if cmd == "apply-gateway-settings":
- if len(sys.argv) != 8:
- print("Usage: oc_config_helper.py apply-gateway-settings ")
+ if len(sys.argv) != 9:
+ print("Usage: oc_config_helper.py apply-gateway-settings ")
sys.exit(1)
mode = sys.argv[2]
- bind_mode = sys.argv[3]
- port = int(sys.argv[4])
- enable_openai_api = sys.argv[5].lower() == "true"
- auth_mode = sys.argv[6]
- trusted_proxies_csv = sys.argv[7]
- success = apply_gateway_settings(mode, bind_mode, port, enable_openai_api, auth_mode, trusted_proxies_csv)
+ remote_url = sys.argv[3]
+ bind_mode = sys.argv[4]
+ port = int(sys.argv[5])
+ enable_openai_api = sys.argv[6].lower() == "true"
+ auth_mode = sys.argv[7]
+ trusted_proxies_csv = sys.argv[8]
+ success = apply_gateway_settings(mode, remote_url, bind_mode, port, enable_openai_api, auth_mode, trusted_proxies_csv)
sys.exit(0 if success else 1)
elif cmd == "get":
@@ -266,11 +287,12 @@ def main():
sys.exit(0)
elif cmd == "set-control-ui-origins":
- if len(sys.argv) != 3:
- print("Usage: oc_config_helper.py set-control-ui-origins ")
+ if len(sys.argv) not in (3, 4):
+ print("Usage: oc_config_helper.py set-control-ui-origins [additional_origins_csv]")
sys.exit(1)
origins_csv = sys.argv[2]
- success = set_control_ui_origins(origins_csv)
+ additional_origins_csv = sys.argv[3] if len(sys.argv) == 4 else ""
+ success = set_control_ui_origins(origins_csv, additional_origins_csv)
sys.exit(0 if success else 1)
elif cmd == "set":
diff --git a/openclaw_assistant/run.sh b/openclaw_assistant/run.sh
index c578e52..93edeb8 100644
--- a/openclaw_assistant/run.sh
+++ b/openclaw_assistant/run.sh
@@ -47,11 +47,13 @@ CLEAN_LOCKS_ON_EXIT=$(jq -r '.clean_session_locks_on_exit // true' "$OPTIONS_FIL
# Gateway configuration
GATEWAY_MODE=$(jq -r '.gateway_mode // "local"' "$OPTIONS_FILE")
+GATEWAY_REMOTE_URL=$(jq -r '.gateway_remote_url // empty' "$OPTIONS_FILE")
GATEWAY_BIND_MODE=$(jq -r '.gateway_bind_mode // "loopback"' "$OPTIONS_FILE")
GATEWAY_PORT=$(jq -r '.gateway_port // 18789' "$OPTIONS_FILE")
ENABLE_OPENAI_API=$(jq -r '.enable_openai_api // false' "$OPTIONS_FILE")
GATEWAY_AUTH_MODE=$(jq -r '.gateway_auth_mode // "token"' "$OPTIONS_FILE")
GATEWAY_TRUSTED_PROXIES=$(jq -r '.gateway_trusted_proxies // empty' "$OPTIONS_FILE")
+GATEWAY_ADDITIONAL_ALLOWED_ORIGINS=$(jq -r '.gateway_additional_allowed_origins // empty' "$OPTIONS_FILE")
FORCE_IPV4_DNS=$(jq -r '.force_ipv4_dns // true' "$OPTIONS_FILE")
ACCESS_MODE=$(jq -r '.access_mode // "custom"' "$OPTIONS_FILE")
NGINX_LOG_LEVEL=$(jq -r '.nginx_log_level // "minimal"' "$OPTIONS_FILE")
@@ -452,8 +454,10 @@ EOF
GW_PID=""
NGINX_PID=""
TTYD_PID=""
+SHUTTING_DOWN="false"
shutdown() {
+ SHUTTING_DOWN="true"
echo "Shutdown requested; stopping services..."
if [ -n "${NGINX_PID}" ] && kill -0 "${NGINX_PID}" >/dev/null 2>&1; then
@@ -534,7 +538,7 @@ if [ -f "$OPENCLAW_CONFIG_PATH" ]; then
if [ -f "$HELPER_PATH" ]; then
# In lan_https mode the gateway uses an internal port; nginx owns the external one.
EFFECTIVE_GW_PORT="$GATEWAY_INTERNAL_PORT"
- if ! python3 "$HELPER_PATH" apply-gateway-settings "$GATEWAY_MODE" "$GATEWAY_BIND_MODE" "$EFFECTIVE_GW_PORT" "$ENABLE_OPENAI_API" "$GATEWAY_AUTH_MODE" "$GATEWAY_TRUSTED_PROXIES"; then
+ if ! python3 "$HELPER_PATH" apply-gateway-settings "$GATEWAY_MODE" "$GATEWAY_REMOTE_URL" "$GATEWAY_BIND_MODE" "$EFFECTIVE_GW_PORT" "$ENABLE_OPENAI_API" "$GATEWAY_AUTH_MODE" "$GATEWAY_TRUSTED_PROXIES"; 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."
@@ -549,11 +553,18 @@ else
echo "INFO: Run 'openclaw onboard' first, then restart the add-on"
fi
+if [ "$GATEWAY_AUTH_MODE" = "trusted-proxy" ]; then
+ echo "NOTICE: gateway_auth_mode=trusted-proxy is enabled."
+ echo "NOTICE: Direct local CLI calls to the gateway may return unauthorized (trusted_proxy_user_missing) unless identity headers are injected by your reverse proxy."
+ echo "NOTICE: For local terminal CLI workflows, temporarily switch to token auth or use commands that don't require direct gateway WS auth."
+fi
+
# ------------------------------------------------------------------------------
# TLS certificate generation for built-in HTTPS proxy (lan_https mode)
# Generates a local CA + server cert so phones/tablets get proper HTTPS.
# The CA cert can be installed once on a device for trusted access.
# ------------------------------------------------------------------------------
+LAN_IP=""
if [ "$ENABLE_HTTPS_PROXY" = "true" ]; then
CERT_DIR="/config/certs"
mkdir -p "$CERT_DIR"
@@ -573,16 +584,46 @@ if [ "$ENABLE_HTTPS_PROXY" = "true" ]; then
echo "INFO: Local CA created at $CERT_DIR/ca.crt"
fi
- # --- Server cert (regenerated when LAN IP changes) ---
- if [ ! -f "$CERT_DIR/gateway.crt" ] || [ ! -f "$CERT_DIR/gateway.key" ] || [ "$LAN_IP" != "$STORED_IP" ]; then
+ # --- Extra SANs from gateway_additional_allowed_origins + gateway_public_url ---
+ EXTRA_SANS=""
+ EXTRA_SAN_SOURCES="${GATEWAY_ADDITIONAL_ALLOWED_ORIGINS},${GW_PUBLIC_URL}"
+ if [ "$EXTRA_SAN_SOURCES" != "," ]; then
+ EXTRA_SANS="$(python3 - "$EXTRA_SAN_SOURCES" "${LAN_IP:-}" <<'PY'
+import sys, re
+from urllib.parse import urlparse
+raw = sys.argv[1] if len(sys.argv) > 1 else ""
+lan_ip = sys.argv[2] if len(sys.argv) > 2 else ""
+entries = [e.strip() for e in raw.split(",") if e.strip()]
+sans = []
+seen = {"127.0.0.1", "localhost", "homeassistant", "homeassistant.local"}
+if lan_ip:
+ seen.add(lan_ip)
+for entry in entries:
+ if "://" not in entry:
+ entry = "https://" + entry
+ host = urlparse(entry).hostname or ""
+ if host and host not in seen:
+ seen.add(host)
+ if re.match(r"^\d{1,3}(\.\d{1,3}){3}$", host):
+ sans.append(f"IP:{host}")
+ else:
+ sans.append(f"DNS:{host}")
+print(",".join(sans), end="")
+PY
+)"
+ fi
+ STORED_EXTRA_SANS=$(cat "$CERT_DIR/.cert_extra_sans" 2>/dev/null || echo "")
+
+ # --- Server cert (regenerated when LAN IP or SANs change) ---
+ if [ ! -f "$CERT_DIR/gateway.crt" ] || [ ! -f "$CERT_DIR/gateway.key" ] || [ "$LAN_IP" != "$STORED_IP" ] || [ "$EXTRA_SANS" != "$STORED_EXTRA_SANS" ]; then
echo "INFO: Generating server TLS certificate for IP: ${LAN_IP:-unknown}..."
openssl genrsa -out "$CERT_DIR/gateway.key" 2048 2>/dev/null
openssl req -new -key "$CERT_DIR/gateway.key" -out "$CERT_DIR/gateway.csr" \
-subj "/CN=OpenClaw Gateway" 2>/dev/null
- # SAN extension — include LAN IP, loopback, and common mDNS names
+ # SAN extension — include LAN IP, loopback, common mDNS names + user extras
cat > "$CERT_DIR/_san.ext" < "$CERT_DIR/.cert_ip"
- echo "INFO: Server TLS certificate generated (SAN includes IP:${LAN_IP:-127.0.0.1})"
+ printf '%s' "$EXTRA_SANS" > "$CERT_DIR/.cert_extra_sans"
+ echo "INFO: Server TLS certificate generated (SAN: IP:${LAN_IP:-127.0.0.1}${EXTRA_SANS:+,${EXTRA_SANS}})"
else
echo "INFO: Reusing existing TLS certificate (IP: $STORED_IP)"
fi
@@ -603,32 +645,44 @@ SANEOF
cp "$CERT_DIR/ca.crt" /etc/nginx/html/openclaw-ca.crt 2>/dev/null || true
echo "INFO: CA certificate available for download at /cert/ca.crt on the HTTPS port"
- # ------------------------------------------------------------------
- # Configure gateway.controlUi for the HTTPS proxy:
- #
- # 1. allowedOrigins — the browser's HTTPS origin must be listed,
- # otherwise v2026.2.21+ rejects with 1008 "origin not allowed".
- #
- # 2. dangerouslyDisableDeviceAuth — skips the interactive device
- # pairing ceremony (1008 "pairing required"). In a self-hosted
- # add-on the user already controls the token, so per-device
- # approval adds friction without real security benefit.
- #
- # NOTE: v2026.2.22+ emits a startup security warning when this
- # flag is active. The warning is expected and harmless for this
- # use case — run `openclaw security audit` for details.
- #
- # Also cleans up any stale keys (e.g. pairingMode) from older
- # add-on versions that would cause "Unrecognized key" errors.
- # ------------------------------------------------------------------
- if [ -n "$LAN_IP" ] && [ -f "$HELPER_PATH" ] && [ -f "$OPENCLAW_CONFIG_PATH" ]; then
+fi
+
+# ------------------------------------------------------------------
+# Configure gateway.controlUi.allowedOrigins:
+# - In lan_https: include HTTPS proxy defaults (LAN IP + common hostnames)
+# - In all modes: also include origin from gateway_public_url when present
+# - Helper merges with existing origins + user extras and deduplicates
+# ------------------------------------------------------------------
+if [ -f "$HELPER_PATH" ] && [ -f "$OPENCLAW_CONFIG_PATH" ]; then
+ ALLOWED_ORIGINS=""
+
+ if [ "$ENABLE_HTTPS_PROXY" = "true" ] && [ -n "$LAN_IP" ]; then
ALLOWED_ORIGINS="https://${LAN_IP}:${GATEWAY_PORT}"
- # Also permit common mDNS/hostname variants so the cert SAN names work too
ALLOWED_ORIGINS="${ALLOWED_ORIGINS},https://homeassistant.local:${GATEWAY_PORT}"
ALLOWED_ORIGINS="${ALLOWED_ORIGINS},https://homeassistant:${GATEWAY_PORT}"
- python3 "$HELPER_PATH" set-control-ui-origins "$ALLOWED_ORIGINS" || \
- echo "WARN: Could not set controlUi settings — gateway may reject the Control UI"
fi
+
+ if [ -n "$GW_PUBLIC_URL" ]; then
+ GW_PUBLIC_ORIGIN="$(python3 - "$GW_PUBLIC_URL" <<'PY'
+import sys
+from urllib.parse import urlparse
+u = (sys.argv[1] or '').strip()
+p = urlparse(u)
+if p.scheme in ('http', 'https') and p.netloc:
+ print(f"{p.scheme}://{p.netloc}", end='')
+PY
+)"
+ if [ -n "$GW_PUBLIC_ORIGIN" ]; then
+ if [ -n "$ALLOWED_ORIGINS" ]; then
+ ALLOWED_ORIGINS="${ALLOWED_ORIGINS},${GW_PUBLIC_ORIGIN}"
+ else
+ ALLOWED_ORIGINS="$GW_PUBLIC_ORIGIN"
+ fi
+ fi
+ fi
+
+ python3 "$HELPER_PATH" set-control-ui-origins "$ALLOWED_ORIGINS" "$GATEWAY_ADDITIONAL_ALLOWED_ORIGINS" || \
+ echo "WARN: Could not set controlUi settings — gateway may reject the Control UI"
fi
# ------------------------------------------------------------------------------
@@ -645,9 +699,53 @@ if [ -f /usr/local/lib/openclaw-proxy-shim.cjs ]; then
export OPENCLAW_GLOBAL_NODE_MODULES
fi
-echo "Starting OpenClaw Assistant gateway (openclaw)..."
-openclaw gateway run &
-GW_PID=$!
+start_openclaw_runtime() {
+ echo "Starting OpenClaw Assistant runtime (openclaw)..."
+ if [ "$GATEWAY_MODE" = "remote" ]; then
+ # Remote mode: do NOT start a local gateway service.
+ # Start a node/client host that connects to the configured remote gateway URL.
+ REMOTE_URL="$(timeout 2s openclaw config get gateway.remote.url 2>/dev/null | tr -d '\n' || true)"
+ if [ -z "$REMOTE_URL" ]; then
+ echo "ERROR: gateway_mode=remote but gateway.remote.url is empty in openclaw config"
+ echo "ERROR: Configure gateway.remote.url first, then restart the add-on"
+ return 1
+ fi
+
+ NODE_HOST=""
+ NODE_PORT=""
+ NODE_TLS_FLAG=""
+ if ! eval "$(python3 - "$REMOTE_URL" <<'PY'
+import sys
+from urllib.parse import urlparse
+url = (sys.argv[1] or '').strip()
+p = urlparse(url)
+if p.scheme not in ('ws', 'wss') or not p.hostname:
+ print('echo "ERROR: Invalid gateway.remote.url (expected ws:// or wss://): %s"' % url.replace('"', '\\"'))
+ print('exit 1')
+ raise SystemExit(0)
+port = p.port or (443 if p.scheme == 'wss' else 80)
+print(f'NODE_HOST={p.hostname}')
+print(f'NODE_PORT={port}')
+print(f'NODE_TLS_FLAG={"--tls" if p.scheme == "wss" else ""}')
+PY
+)"; then
+ echo "ERROR: Failed to parse gateway.remote.url: $REMOTE_URL"
+ return 1
+ fi
+
+ echo "INFO: gateway_mode=remote detected; starting node host to $NODE_HOST:$NODE_PORT ${NODE_TLS_FLAG}"
+ # shellcheck disable=SC2086
+ openclaw node run --host "$NODE_HOST" --port "$NODE_PORT" $NODE_TLS_FLAG &
+ else
+ openclaw gateway run &
+ fi
+ GW_PID=$!
+ return 0
+}
+
+if ! start_openclaw_runtime; then
+ exit 1
+fi
# Start web terminal (optional)
TTYD_PID_FILE="/var/run/openclaw-ttyd.pid"
@@ -717,8 +815,12 @@ fi
# 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)"
+# Read directly from config file — the CLI redacts secrets since v2026.2.22+.
+GW_TOKEN="$(python3 -c "
+import json, os
+p = os.environ.get('OPENCLAW_CONFIG_PATH', '/config/.openclaw/openclaw.json')
+print(json.load(open(p)).get('gateway',{}).get('auth',{}).get('token',''), end='')
+" 2>/dev/null || true)"
# Collect disk usage for landing page status card
DISK_TOTAL="" DISK_USED="" DISK_AVAIL="" DISK_PCT=""
@@ -755,5 +857,21 @@ else
echo "WARN: nginx failed to start (PID $NGINX_PID exited); ingress UI may be unavailable"
fi
-# Wait for gateway; if it exits, shut down others.
-wait "${GW_PID}"
+# Keep add-on alive even if gateway/node runtime restarts itself (e.g. during onboarding).
+# If runtime exits unexpectedly, restart it while nginx/ttyd stay up.
+while true; do
+ GW_EXIT_CODE=0
+ wait "${GW_PID}" || GW_EXIT_CODE=$?
+
+ if [ "$SHUTTING_DOWN" = "true" ]; then
+ break
+ fi
+
+ echo "WARN: OpenClaw runtime exited with code ${GW_EXIT_CODE}. Restarting in 2s..."
+ sleep 2
+
+ if ! start_openclaw_runtime; then
+ echo "ERROR: Failed to restart OpenClaw runtime; retrying in 5s..."
+ sleep 5
+ fi
+done
diff --git a/openclaw_assistant/translations/bg.yaml b/openclaw_assistant/translations/bg.yaml
index 03ea571..ec05aad 100644
--- a/openclaw_assistant/translations/bg.yaml
+++ b/openclaw_assistant/translations/bg.yaml
@@ -50,6 +50,14 @@ configuration:
local: "Локален (препоръчително)"
remote: "Отдалечен"
+ gateway_remote_url:
+ name: URL на отдалечен Gateway
+ description: WebSocket URL на отдалечения gateway, използван когато Gateway Mode е remote (пример - ws://192.168.1.20:18789 или wss://gateway.example.com:443)
+
+ gateway_additional_allowed_origins:
+ name: Допълнителни разрешени origins
+ description: Допълнителни origins за Control UI, разделени със запетая; сливат се в gateway.controlUi.allowedOrigins при lan_https (пример - https://ha.example.com:8443,capacitor://localhost)
+
gateway_bind_mode:
name: Режим на свързване на Gateway
description: Режим на мрежово свързване - loopback (само 127.0.0.1, най-сигурен), lan (всички интерфейси) или tailnet (само Tailscale интерфейс). Пренебрегва се от предварителните режими на достъп.
diff --git a/openclaw_assistant/translations/de.yaml b/openclaw_assistant/translations/de.yaml
index 49448ad..b20f1a2 100644
--- a/openclaw_assistant/translations/de.yaml
+++ b/openclaw_assistant/translations/de.yaml
@@ -50,6 +50,14 @@ configuration:
local: "Lokal (empfohlen)"
remote: "Remote"
+ gateway_remote_url:
+ name: Remote Gateway-URL
+ description: Remote-Gateway-WebSocket-URL, verwendet wenn Gateway-Modus auf Remote steht (Beispiel - ws://192.168.1.20:18789 oder wss://gateway.example.com:443)
+
+ gateway_additional_allowed_origins:
+ name: Zusätzliche erlaubte Origins
+ description: Kommagetrennte zusätzliche Control-UI-Origins, die bei lan_https in gateway.controlUi.allowedOrigins zusammengeführt werden (Beispiel - https://ha.example.com:8443,capacitor://localhost)
+
gateway_bind_mode:
name: Gateway-Bindungsmodus
description: Netzwerk-Bindungsmodus - loopback (nur 127.0.0.1, am sichersten), lan (alle Schnittstellen) oder tailnet (nur Tailscale-Schnittstelle). Wird durch Zugriffsmodus-Voreinstellungen überschrieben.
diff --git a/openclaw_assistant/translations/en.yaml b/openclaw_assistant/translations/en.yaml
index cd2cd70..08b00ac 100644
--- a/openclaw_assistant/translations/en.yaml
+++ b/openclaw_assistant/translations/en.yaml
@@ -50,6 +50,14 @@ configuration:
local: "Local (recommended)"
remote: "Remote"
+ gateway_remote_url:
+ name: Remote Gateway URL
+ description: Remote gateway WebSocket URL used when Gateway Mode is Remote (example - ws://192.168.1.20:18789 or wss://gateway.example.com:443)
+
+ gateway_additional_allowed_origins:
+ name: Additional Allowed Origins
+ description: Comma-separated extra Control UI origins to merge into gateway.controlUi.allowedOrigins in lan_https mode (example - https://ha.example.com:8443,capacitor://localhost)
+
gateway_bind_mode:
name: Gateway Bind Mode
description: Network bind mode - loopback (127.0.0.1 only, most secure), lan (all interfaces), or tailnet (Tailscale interface only). Overridden by access_mode presets.
diff --git a/openclaw_assistant/translations/es.yaml b/openclaw_assistant/translations/es.yaml
index 771716f..9a1bf55 100644
--- a/openclaw_assistant/translations/es.yaml
+++ b/openclaw_assistant/translations/es.yaml
@@ -50,6 +50,14 @@ configuration:
local: "Local (recomendado)"
remote: "Remoto"
+ gateway_remote_url:
+ name: URL del Gateway Remoto
+ description: URL WebSocket del gateway remoto utilizada cuando Gateway Mode es Remote (ejemplo - ws://192.168.1.20:18789 o wss://gateway.example.com:443)
+
+ gateway_additional_allowed_origins:
+ name: Orígenes permitidos adicionales
+ description: Orígenes extra de Control UI separados por comas que se combinan en gateway.controlUi.allowedOrigins en modo lan_https (ejemplo - https://ha.example.com:8443,capacitor://localhost)
+
gateway_bind_mode:
name: Modo de enlace del Gateway
description: "Modo de enlace de red: loopback (solo 127.0.0.1, más seguro), lan (todas las interfaces) o tailnet (solo interfaz Tailscale). Se anula con las preselecciones del modo de acceso."
diff --git a/openclaw_assistant/translations/pl.yaml b/openclaw_assistant/translations/pl.yaml
index d7dadf0..e0310ff 100644
--- a/openclaw_assistant/translations/pl.yaml
+++ b/openclaw_assistant/translations/pl.yaml
@@ -43,6 +43,14 @@ configuration:
name: Wyczyść blokady sesji przy wyjściu
description: Usuń pliki blokad sesji gdy dodatek zatrzymuje się poprawnie
+ gateway_remote_url:
+ name: URL zdalnego Gateway
+ description: Adres WebSocket zdalnego gateway używany gdy Gateway Mode = Remote (przykład - ws://192.168.1.20:18789 lub wss://gateway.example.com:443)
+
+ gateway_additional_allowed_origins:
+ name: Dodatkowe dozwolone origins
+ description: Dodatkowe origins Control UI rozdzielone przecinkami; łączone z gateway.controlUi.allowedOrigins w trybie lan_https (przykład - https://ha.example.com:8443,capacitor://localhost)
+
gateway_bind_mode:
name: Tryb bindowania Gateway
description: Tryb bindowania sieci - loopback (tylko 127.0.0.1, najbezpieczniejszy), lan (wszystkie interfejsy) lub tailnet (tylko interfejs Tailscale). Nadpisywane przez ustawienia trybu dostępu.
diff --git a/openclaw_assistant/translations/pt-BR.yaml b/openclaw_assistant/translations/pt-BR.yaml
index 8207da3..65b1603 100644
--- a/openclaw_assistant/translations/pt-BR.yaml
+++ b/openclaw_assistant/translations/pt-BR.yaml
@@ -50,6 +50,14 @@ configuration:
local: "Local (recomendado)"
remote: "Remoto"
+ gateway_remote_url:
+ name: URL do Gateway Remoto
+ description: URL WebSocket do gateway remoto usada quando Gateway Mode está em Remote (exemplo - ws://192.168.1.20:18789 ou wss://gateway.example.com:443)
+
+ gateway_additional_allowed_origins:
+ name: Origens permitidas adicionais
+ description: Origens extras da Control UI separadas por vírgula, mescladas em gateway.controlUi.allowedOrigins no modo lan_https (exemplo - https://ha.example.com:8443,capacitor://localhost)
+
gateway_bind_mode:
name: Modo de Vinculação do Gateway
description: Modo de vinculação de rede - loopback (somente 127.0.0.1, mais seguro), lan (todas as interfaces) ou tailnet (somente interface Tailscale). Substituído pelas predefinições do modo de acesso.