diff --git a/openclaw_assistant/run.sh b/openclaw_assistant/run.sh index 22e0313..dd78ee1 100644 --- a/openclaw_assistant/run.sh +++ b/openclaw_assistant/run.sh @@ -9,47 +9,37 @@ if [ ! -f "$OPTIONS_FILE" ]; then exit 1 fi +# ------------------------------------------------------------------------------ +# Read add-on options (ALL optional; onboarding can fill the rest) +# ------------------------------------------------------------------------------ + BOT_TOKEN=$(jq -r '.telegram_bot_token // empty' "$OPTIONS_FILE") TZNAME=$(jq -r '.timezone // "Europe/Sofia"' "$OPTIONS_FILE") ALLOW_FROM_RAW=$(jq -r '.telegram_allow_from // empty' "$OPTIONS_FILE") -MODEL_PRIMARY=$(jq -r '.model_primary // "openai-codex/gpt-5.2"' "$OPTIONS_FILE") -GW_BIND=$(jq -r '.gateway_bind // "loopback"' "$OPTIONS_FILE") -GW_PORT=$(jq -r '.gateway_port // 18789' "$OPTIONS_FILE") +MODEL_PRIMARY=$(jq -r '.model_primary // empty' "$OPTIONS_FILE") +GW_BIND=$(jq -r '.gateway_bind // empty' "$OPTIONS_FILE") +GW_PORT=$(jq -r '.gateway_port // empty' "$OPTIONS_FILE") GW_TOKEN=$(jq -r '.gateway_token // empty' "$OPTIONS_FILE") GW_PUBLIC_URL=$(jq -r '.gateway_public_url // empty' "$OPTIONS_FILE") HA_TOKEN=$(jq -r '.homeassistant_token // empty' "$OPTIONS_FILE") BRAVE_KEY=$(jq -r '.brave_api_key // empty' "$OPTIONS_FILE") ENABLE_TERMINAL=$(jq -r '.enable_terminal // false' "$OPTIONS_FILE") -# Generic router SSH settings (preferred) + +# 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") -# Backward compatible (deprecated) MikroTik-named settings -MT_HOST=$(jq -r '.mikrotik_host // empty' "$OPTIONS_FILE") -MT_USER=$(jq -r '.mikrotik_ssh_user // empty' "$OPTIONS_FILE") -MT_KEY=$(jq -r '.mikrotik_ssh_key_path // "/data/keys/mikrotik"' "$OPTIONS_FILE") - -# Prefer generic router_* values; fall back to mikrotik_* if router_* are empty -[ -z "$ROUTER_HOST" ] && ROUTER_HOST="$MT_HOST" -[ -z "$ROUTER_USER" ] && ROUTER_USER="$MT_USER" -[ "$ROUTER_KEY" = "/data/keys/router_ssh" ] && [ -n "$MT_KEY" ] && ROUTER_KEY="$MT_KEY" - # 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") -if [ -z "$BOT_TOKEN" ]; then - echo "You must set telegram_bot_token in the add-on configuration." - exit 1 -fi - 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). -# Use /config as HOME so OpenClaw finds its auth store and config there. export HOME=/config mkdir -p /config/.openclaw /config/clawd /config/keys /config/secrets @@ -76,7 +66,6 @@ fi # Session lock cleanup helpers # ------------------------------------------------------------------------------ -# Returns 0 if a gateway process appears to be running, else 1 gateway_running() { pgrep -f "openclaw.*gateway.*run" >/dev/null 2>&1 } @@ -104,7 +93,6 @@ cleanup_session_locks() { rm -f "${sessions_dir}"/*.jsonl.lock || true } -# Cleanup on start (stale locks after crashes/restarts) if [ "$CLEAN_LOCKS_ON_START" = "true" ]; then cleanup_session_locks else @@ -115,109 +103,109 @@ fi # Store tokens / export env vars (optional) # ------------------------------------------------------------------------------ -# Home Assistant long-lived token (for local HA API scripts/tools) if [ -n "$HA_TOKEN" ]; then umask 077 printf '%s' "$HA_TOKEN" > /config/secrets/homeassistant.token fi -# Brave Search API key (for OpenClaw's web_search tool, which reads BRAVE_API_KEY) if [ -n "$BRAVE_KEY" ]; then export BRAVE_API_KEY="$BRAVE_KEY" umask 077 printf '%s' "$BRAVE_KEY" > /config/secrets/brave_api_key fi -# Decide Telegram DM access policy. -DM_POLICY="pairing" -ALLOW_FROM_JSON="" -if [ -n "$ALLOW_FROM_RAW" ]; then - DM_POLICY="allowlist" - # convert "1,2, 3" -> ["1","2","3"] using jq (no python dependency) - ALLOW_FROM_JSON=$(printf '%s' "$ALLOW_FROM_RAW" | jq -R 'split(",") | map(gsub("^\\s+|\\s+$"; "")) | map(select(length>0))') -fi +# ------------------------------------------------------------------------------ +# Non-invasive OpenClaw config management +# - If config is missing: create it. +# - If config exists: ONLY patch the fields whose add-on options are set. +# - If config is not parseable as strict JSON (e.g. JSON5): do NOT touch it. +# (This keeps onboarding-managed config intact.) +# ------------------------------------------------------------------------------ -# Validate gateway exposure settings -if [ "$GW_BIND" = "lan" ] && [ -z "$GW_TOKEN" ]; then - echo "ERROR: gateway_bind=lan requires gateway_token to be set (do not expose an unauthenticated gateway)." - exit 1 -fi +OPENCLAW_CONFIG_PATH="/config/.openclaw/openclaw.json" -GW_AUTH_BLOCK="auth: { mode: \"token\", token: \"${GW_TOKEN}\" }" -if [ -z "$GW_TOKEN" ]; then - # Let doctor generate one (loopback-only is still protected by local access) - GW_AUTH_BLOCK="auth: { mode: \"token\" }" -fi +python3 - <<'PY' +import json +import os +from pathlib import Path -# Write gateway config (JSON5). -mkdir -p /config/.openclaw -cat > /config/.openclaw/openclaw.json <}" +# Patch only if env var is set (non-empty) -# Auth store debug (redacted): never print tokens -AUTH_STORE="/config/.openclaw/agents/main/agent/auth-profiles.json" -if [ -f "$AUTH_STORE" ]; then - echo "Auth store present at $AUTH_STORE" - echo "Auth store summary (redacted):" - jq -r '"version="+((.version//"?")|tostring), - "profiles="+(((.profiles//{})|keys|join(","))), - "providers="+(((.profiles//{})|to_entries|map(.value.provider // "?")|unique|join(",")))' "$AUTH_STORE" 2>/dev/null || echo "(could not parse auth store JSON)" -else - echo "Auth store not found at $AUTH_STORE" -fi +def env(name): + v = os.environ.get(name, '') + return v if v != '' else None -echo "Sanity check: DNS + Telegram API reachability" -if curl -fsS --max-time 10 https://api.telegram.org/ >/dev/null; then - echo "OK: api.telegram.org reachable" -else - echo "WARN: api.telegram.org not reachable from add-on container" -fi +# gateway bind/port/token +bind_ = env('GW_BIND') +if bind_: + set_path(cfg, ['gateway', 'bind'], bind_) -# Validate token (print only bot username) -BOT_USER=$(curl -fsS --max-time 10 "https://api.telegram.org/bot${BOT_TOKEN}/getMe" | jq -r '.result.username // empty' || true) -if [ -n "$BOT_USER" ]; then - echo "OK: Telegram token valid for @${BOT_USER}" -else - echo "WARN: Telegram token validation failed (getMe)" -fi +port_ = env('GW_PORT') +if port_: + try: + set_path(cfg, ['gateway', 'port'], int(port_)) + except Exception: + pass + +token_ = env('GW_TOKEN') +if token_: + set_path(cfg, ['gateway', 'auth', 'mode'], 'token') + set_path(cfg, ['gateway', 'auth', 'token'], token_) + +# Agent defaults: workspace + primary model +# We always ensure workspace points to the add-on workspace (safe and expected). +set_path(cfg, ['agents', 'defaults', 'workspace'], '/config/clawd') + +model_primary = env('MODEL_PRIMARY') +if model_primary: + set_path(cfg, ['agents', 'defaults', 'model', 'primary'], model_primary) + # Ensure models entry exists (minimal) + models = cfg.get('agents', {}).get('defaults', {}).get('models', {}) + if not isinstance(models, dict): + models = {} + models.setdefault(model_primary, {}) + set_path(cfg, ['agents', 'defaults', 'models'], models) + +# Telegram channel only if token provided in options +bot_token = env('BOT_TOKEN') +if bot_token: + set_path(cfg, ['channels', 'telegram', 'enabled'], True) + set_path(cfg, ['channels', 'telegram', 'botToken'], bot_token) + + allow_from_raw = env('ALLOW_FROM_RAW') + if allow_from_raw: + ids = [x.strip() for x in allow_from_raw.split(',') if x.strip()] + if ids: + set_path(cfg, ['channels', 'telegram', 'dmPolicy'], 'allowlist') + set_path(cfg, ['channels', 'telegram', 'allowFrom'], ids) + else: + # If not set, do NOT override dmPolicy/allowFrom; let onboarding decide. + pass + +# Write (pretty JSON; JSON is valid JSON5 too) +cfg_path.parent.mkdir(parents=True, exist_ok=True) +cfg_path.write_text(json.dumps(cfg, indent=2, sort_keys=True) + "\n", encoding='utf-8') +print(f"INFO: OpenClaw config written/patched at {cfg_path}") +PY # Convenience info for later (router SSH access path & HA token file) cat > /config/CONNECTION_NOTES.txt </dev/null 2>&1; then - CLI_BIN="openclaw" -else +if ! command -v openclaw >/dev/null 2>&1; then echo "ERROR: openclaw is not installed." exit 1 fi -echo "Starting OpenClaw Assistant gateway (${CLI_BIN}-compatible)..." -"${CLI_BIN}" gateway run & +echo "Starting OpenClaw Assistant gateway (openclaw)..." +openclaw gateway run & GW_PID=$! # Start web terminal (optional) if [ "$ENABLE_TERMINAL" = "true" ]; then echo "Starting web terminal (ttyd) on 127.0.0.1:7681 ..." - # -W: allow clients to write (interactive) - # -p: port - # bind localhost only; exposed to HA via ingress reverse proxy - # ttyd is writable by default; use -R for read-only. (Some builds don't support -W) - # -b sets the base path so it works behind Ingress (/terminal/) ttyd -i 127.0.0.1 -p 7681 -b /terminal bash & TTYD_PID=$! else @@ -310,9 +290,6 @@ tpl = Path('/etc/nginx/nginx.conf.tpl').read_text() token = os.environ.get('GW_TOKEN','') public_url = os.environ.get('GW_PUBLIC_URL','') -# Normalize gateway_public_url for building the link: ensure there is exactly one slash -# between base URL and path. We keep the displayed base as-is, but compute a safe suffix. -# If base ends with '/', use empty suffix. Otherwise use '/'. conf = tpl.replace('__GATEWAY_TOKEN__', token) conf = conf.replace('__GATEWAY_PUBLIC_URL__', public_url) conf = conf.replace('__GW_PUBLIC_URL_PATH__', '' if public_url.endswith('/') else '/')