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.