Make openclaw.json management non-invasive (create if missing, patch only set options)
This commit is contained in:
+100
-123
@@ -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/<slug> 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 <<EOF
|
||||
{
|
||||
discovery: { wideArea: { enabled: false } },
|
||||
cfg_path = Path(os.environ['OPENCLAW_CONFIG_PATH'])
|
||||
|
||||
gateway: {
|
||||
mode: "local",
|
||||
bind: "${GW_BIND}",
|
||||
port: ${GW_PORT},
|
||||
controlUi: { allowInsecureAuth: true },
|
||||
${GW_AUTH_BLOCK}
|
||||
},
|
||||
agents: {
|
||||
defaults: {
|
||||
workspace: "/config/clawd",
|
||||
model: { primary: "${MODEL_PRIMARY}" },
|
||||
models: {
|
||||
"${MODEL_PRIMARY}": {}
|
||||
}
|
||||
},
|
||||
list: [
|
||||
{ id: "main" }
|
||||
]
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "${BOT_TOKEN}",
|
||||
dmPolicy: "${DM_POLICY}"${ALLOW_FROM_RAW:+,
|
||||
allowFrom: ${ALLOW_FROM_JSON}}
|
||||
}
|
||||
}
|
||||
}
|
||||
EOF
|
||||
def set_path(d, keys, value):
|
||||
cur = d
|
||||
for k in keys[:-1]:
|
||||
if k not in cur or not isinstance(cur[k], dict):
|
||||
cur[k] = {}
|
||||
cur = cur[k]
|
||||
cur[keys[-1]] = value
|
||||
|
||||
# OpenClaw reads its config from /config/.openclaw/openclaw.json
|
||||
# Load existing config if possible
|
||||
cfg = {}
|
||||
if cfg_path.exists():
|
||||
try:
|
||||
cfg = json.loads(cfg_path.read_text(encoding='utf-8'))
|
||||
except Exception as e:
|
||||
print(f"INFO: {cfg_path} exists but is not strict JSON; leaving it untouched ({e}).")
|
||||
raise SystemExit(0)
|
||||
|
||||
echo "Model primary=${MODEL_PRIMARY}"
|
||||
echo "Gateway bind=${GW_BIND} port=${GW_PORT} token=${GW_TOKEN:+(set)}${GW_TOKEN:-(auto)}"
|
||||
echo "Telegram dmPolicy=${DM_POLICY}${ALLOW_FROM_RAW:+ (allowFrom=${ALLOW_FROM_RAW})}"
|
||||
echo "Telegram allowFrom JSON: ${ALLOW_FROM_JSON:-<none>}"
|
||||
# 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 <<EOF
|
||||
@@ -230,9 +218,10 @@ EOF
|
||||
|
||||
RUN_DOCTOR=$(jq -r '.run_doctor_on_start // false' "$OPTIONS_FILE")
|
||||
|
||||
# Run doctor ONLY if explicitly enabled; otherwise don't touch user-managed config/state.
|
||||
if [ "$RUN_DOCTOR" = "true" ]; then
|
||||
echo "Running assistant doctor (auto-fix) ..."
|
||||
(timeout 60s "${CLI_BIN:-openclaw}" doctor --fix --yes) || true
|
||||
(timeout 60s openclaw doctor --fix --yes) || true
|
||||
else
|
||||
echo "Skipping doctor on startup (run_doctor_on_start=false)"
|
||||
fi
|
||||
@@ -241,6 +230,8 @@ fi
|
||||
# Graceful shutdown handling (PID 1 trap) to reduce stale locks
|
||||
# ------------------------------------------------------------------------------
|
||||
GW_PID=""
|
||||
NGINX_PID=""
|
||||
TTYD_PID=""
|
||||
|
||||
shutdown() {
|
||||
echo "Shutdown requested; stopping services..."
|
||||
@@ -267,29 +258,18 @@ shutdown() {
|
||||
|
||||
trap shutdown INT TERM
|
||||
|
||||
NGINX_PID=""
|
||||
TTYD_PID=""
|
||||
|
||||
CLI_BIN=""
|
||||
if command -v openclaw >/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 '/')
|
||||
|
||||
Reference in New Issue
Block a user