Spaces:
Running
Harden config build, speed up startup, add healthcheck
Browse filesstart.sh:
- CLOUDFLARE_PROXY_DEBUG defaults to false (Gemini path verified working;
per-request "Redirecting" log is no longer needed by default).
- Replace shell-interpolated jq calls with --arg/--argjson for
GATEWAY_TOKEN, LLM_MODEL, OPENCLAW_PASSWORD, BROWSER_EXECUTABLE_PATH,
SPACE_HOST, TELEGRAM_USER_ID. Prevents JSON breakage and jq filter
injection if those values contain quotes/backslashes/dollar signs.
- Combine ~6 sequential jq invocations (token + model + logging,
plugin allow/deny + entry toggles, controlUi + password) into single
pipelines. Saves several hundred ms of subprocess overhead on cold start.
- Document why device-pair/phone-control/talk-voice are pre-allowed and
why lmstudio/xai PLUGINS (not the xai model provider) are denied.
- Replace `sleep 3 && kill -0 $!` readiness check with a TCP poll on
127.0.0.1:7860 (configurable via GATEWAY_READY_TIMEOUT, default 90s)
that also bails out if the pipeline died. Old check tested the tee PID
in the pipeline, not openclaw, so late crashes went undetected.
- Drop remaining emoji prefixes from error/warning prints.
cloudflare-proxy.js:
- Tighten the require() hook so undici patching only fires for the bare
"undici" id or paths whose final package segment is undici. The old
substring check `id.includes("/undici/")` would also catch unrelated
packages like "super-undici-x".
cloudflare-proxy-setup.py:
- Always write CF_PROXY_ENV_FILE when CLOUDFLARE_PROXY_URL is provided,
even with an empty CLOUDFLARE_PROXY_SECRET, so start.sh's
`. $CF_PROXY_ENV_FILE` reliably exports the URL. Print a warning when
the URL is set without a secret (silent 401 is the worst failure mode).
- chmod 0600 the env file explicitly (umask 0077 should already cover
it; this is belt-and-suspenders since the file holds the worker
shared secret).
workspace-sync.py:
- After restoring WhatsApp credentials, walk the directory and chmod
0700 on dirs / 0600 on files so session secrets aren't world-readable.
Dockerfile:
- Add HEALTHCHECK against http://localhost:7861/health (health-server
proxies to the gateway). 90s start-period covers cold-start plugin
install.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
- Dockerfile +5 -0
- cloudflare-proxy-setup.py +23 -7
- cloudflare-proxy.js +7 -1
- start.sh +111 -61
- workspace-sync.py +10 -0
|
@@ -85,4 +85,9 @@ WORKDIR /home/node/app
|
|
| 85 |
|
| 86 |
EXPOSE 7861
|
| 87 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 88 |
CMD ["/home/node/app/start.sh"]
|
|
|
|
| 85 |
|
| 86 |
EXPOSE 7861
|
| 87 |
|
| 88 |
+
# health-server.js exposes /health on 7861 and proxies to the gateway on 7860.
|
| 89 |
+
# 90s start period covers OpenClaw's plugin install + gateway boot on cold start.
|
| 90 |
+
HEALTHCHECK --interval=30s --timeout=5s --start-period=90s \
|
| 91 |
+
CMD curl -fsS http://localhost:7861/health || exit 1
|
| 92 |
+
|
| 93 |
CMD ["/home/node/app/start.sh"]
|
|
@@ -154,6 +154,12 @@ def write_env(proxy_url: str, proxy_secret: str) -> None:
|
|
| 154 |
+ "\n",
|
| 155 |
encoding="utf-8",
|
| 156 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 157 |
|
| 158 |
|
| 159 |
def main() -> int:
|
|
@@ -162,10 +168,20 @@ def main() -> int:
|
|
| 162 |
api_token = os.environ.get("CLOUDFLARE_WORKERS_TOKEN", "").strip()
|
| 163 |
|
| 164 |
if existing_url:
|
| 165 |
-
|
|
|
|
|
|
|
|
|
|
| 166 |
write_env(existing_url, existing_secret)
|
| 167 |
-
|
| 168 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 169 |
|
| 170 |
if not api_token:
|
| 171 |
return 0
|
|
@@ -214,22 +230,22 @@ def main() -> int:
|
|
| 214 |
|
| 215 |
proxy_url = f"https://{worker_name}.{subdomain}.workers.dev"
|
| 216 |
write_env(proxy_url, proxy_secret)
|
| 217 |
-
print(f"
|
| 218 |
return 0
|
| 219 |
except urllib.error.HTTPError as error:
|
| 220 |
detail = error.read().decode("utf-8", errors="replace")
|
| 221 |
if error.code == 403 and '"code":9109' in detail:
|
| 222 |
print(
|
| 223 |
-
"
|
| 224 |
"Use a Cloudflare API Token in CLOUDFLARE_WORKERS_TOKEN "
|
| 225 |
"(not a Global API Key, tunnel token, or worker secret). "
|
| 226 |
"For auto-setup, it should have account-level 'Workers Scripts: Edit'. "
|
| 227 |
"The setup can auto-discover your account; CLOUDFLARE_ACCOUNT_ID is not required."
|
| 228 |
)
|
| 229 |
-
print(f"
|
| 230 |
return 1
|
| 231 |
except Exception as error:
|
| 232 |
-
print(f"
|
| 233 |
return 1
|
| 234 |
|
| 235 |
|
|
|
|
| 154 |
+ "\n",
|
| 155 |
encoding="utf-8",
|
| 156 |
)
|
| 157 |
+
# Belt-and-suspenders: even with umask 0077 on the parent shell, force
|
| 158 |
+
# 0600 since the file holds the worker shared secret.
|
| 159 |
+
try:
|
| 160 |
+
ENV_FILE.chmod(0o600)
|
| 161 |
+
except OSError:
|
| 162 |
+
pass
|
| 163 |
|
| 164 |
|
| 165 |
def main() -> int:
|
|
|
|
| 168 |
api_token = os.environ.get("CLOUDFLARE_WORKERS_TOKEN", "").strip()
|
| 169 |
|
| 170 |
if existing_url:
|
| 171 |
+
# Always write the env file so downstream `. $CF_PROXY_ENV_FILE` in
|
| 172 |
+
# start.sh has CLOUDFLARE_PROXY_URL set even when no secret was
|
| 173 |
+
# supplied. Empty secret means we send no x-proxy-key header β that
|
| 174 |
+
# only works if the deployed worker also has no secret baked in.
|
| 175 |
write_env(existing_url, existing_secret)
|
| 176 |
+
if not existing_secret:
|
| 177 |
+
print(
|
| 178 |
+
"Warning: CLOUDFLARE_PROXY_URL is set but CLOUDFLARE_PROXY_SECRET "
|
| 179 |
+
"is empty. Requests will succeed only if the deployed worker "
|
| 180 |
+
"was built without PROXY_SHARED_SECRET; otherwise you'll see "
|
| 181 |
+
"401 Unauthorized."
|
| 182 |
+
)
|
| 183 |
+
print(f"Using configured Cloudflare proxy: {existing_url}")
|
| 184 |
+
return 0
|
| 185 |
|
| 186 |
if not api_token:
|
| 187 |
return 0
|
|
|
|
| 230 |
|
| 231 |
proxy_url = f"https://{worker_name}.{subdomain}.workers.dev"
|
| 232 |
write_env(proxy_url, proxy_secret)
|
| 233 |
+
print(f"Cloudflare proxy ready: {proxy_url}")
|
| 234 |
return 0
|
| 235 |
except urllib.error.HTTPError as error:
|
| 236 |
detail = error.read().decode("utf-8", errors="replace")
|
| 237 |
if error.code == 403 and '"code":9109' in detail:
|
| 238 |
print(
|
| 239 |
+
"Cloudflare proxy setup failed: invalid Workers token. "
|
| 240 |
"Use a Cloudflare API Token in CLOUDFLARE_WORKERS_TOKEN "
|
| 241 |
"(not a Global API Key, tunnel token, or worker secret). "
|
| 242 |
"For auto-setup, it should have account-level 'Workers Scripts: Edit'. "
|
| 243 |
"The setup can auto-discover your account; CLOUDFLARE_ACCOUNT_ID is not required."
|
| 244 |
)
|
| 245 |
+
print(f"Cloudflare proxy setup failed: HTTP {error.code} {detail}")
|
| 246 |
return 1
|
| 247 |
except Exception as error:
|
| 248 |
+
print(f"Cloudflare proxy setup failed: {error}")
|
| 249 |
return 1
|
| 250 |
|
| 251 |
|
|
@@ -337,11 +337,17 @@ if (PROXY_URL) {
|
|
| 337 |
patchUndiciInstance(undici);
|
| 338 |
} catch (e) {}
|
| 339 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 340 |
const Module = require("module");
|
| 341 |
const originalRequire = Module.prototype.require;
|
|
|
|
| 342 |
Module.prototype.require = function (id) {
|
| 343 |
const exports = originalRequire.apply(this, arguments);
|
| 344 |
-
if (id === "undici" ||
|
| 345 |
try { patchUndiciInstance(exports); } catch (e) {}
|
| 346 |
}
|
| 347 |
return exports;
|
|
|
|
| 337 |
patchUndiciInstance(undici);
|
| 338 |
} catch (e) {}
|
| 339 |
|
| 340 |
+
// Hook require() to patch any undici instance the moment it loads.
|
| 341 |
+
// Match either the bare "undici" id or paths whose final package
|
| 342 |
+
// segment IS undici (e.g. "/foo/node_modules/undici/index.js"). The
|
| 343 |
+
// earlier substring check `id.includes("/undici/")` would also match
|
| 344 |
+
// unrelated packages like "super-undici-x".
|
| 345 |
const Module = require("module");
|
| 346 |
const originalRequire = Module.prototype.require;
|
| 347 |
+
const UNDICI_PATH_RE = /(?:^|\/)node_modules\/undici(?:\/|$)/;
|
| 348 |
Module.prototype.require = function (id) {
|
| 349 |
const exports = originalRequire.apply(this, arguments);
|
| 350 |
+
if (id === "undici" || UNDICI_PATH_RE.test(id)) {
|
| 351 |
try { patchUndiciInstance(exports); } catch (e) {}
|
| 352 |
}
|
| 353 |
return exports;
|
|
@@ -38,13 +38,13 @@ echo ""
|
|
| 38 |
# ββ Validate required secrets ββ
|
| 39 |
ERRORS=""
|
| 40 |
if [ -z "$LLM_API_KEY" ]; then
|
| 41 |
-
ERRORS="${ERRORS}
|
| 42 |
fi
|
| 43 |
if [ -z "$LLM_MODEL" ]; then
|
| 44 |
-
ERRORS="${ERRORS}
|
| 45 |
fi
|
| 46 |
if [ -z "$GATEWAY_TOKEN" ]; then
|
| 47 |
-
ERRORS="${ERRORS}
|
| 48 |
fi
|
| 49 |
if [ -n "$ERRORS" ]; then
|
| 50 |
echo "Missing required secrets:"
|
|
@@ -73,7 +73,7 @@ fi
|
|
| 73 |
# Auto-correct Gemini models to use google/ prefix if anthropic/ was mistakenly used
|
| 74 |
if [[ "$LLM_MODEL" == "anthropic/gemini"* ]]; then
|
| 75 |
LLM_MODEL=$(echo "$LLM_MODEL" | sed 's/^anthropic\//google\//')
|
| 76 |
-
echo "
|
| 77 |
fi
|
| 78 |
|
| 79 |
# Extract provider prefix from model name (e.g. "google/gemini-2.5-flash" β "google")
|
|
@@ -146,7 +146,9 @@ export CLOUDFLARE_WORKERS_TOKEN
|
|
| 146 |
CF_PROXY_ENV_FILE="/tmp/huggingclaw-cloudflare-proxy.env"
|
| 147 |
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
|
| 148 |
export CLOUDFLARE_PROXY_DOMAINS="${CLOUDFLARE_PROXY_DOMAINS:-api.telegram.org,web.whatsapp.com,googleapis.com}"
|
| 149 |
-
|
|
|
|
|
|
|
| 150 |
echo "Preparing Cloudflare outbound proxy..."
|
| 151 |
python3 /home/node/app/cloudflare-proxy-setup.py || true
|
| 152 |
if [ -f "$CF_PROXY_ENV_FILE" ]; then
|
|
@@ -183,12 +185,20 @@ CONFIG_JSON=$(cat <<'CONFIGEOF'
|
|
| 183 |
CONFIGEOF
|
| 184 |
)
|
| 185 |
|
| 186 |
-
#
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
|
| 193 |
# Optional: dynamic custom OpenAI-compatible provider registration
|
| 194 |
CUSTOM_PROVIDER_NAME="${CUSTOM_PROVIDER_NAME:-}"
|
|
@@ -206,29 +216,29 @@ if [ -n "$CUSTOM_PROVIDER_NAME" ] || [ -n "$CUSTOM_BASE_URL" ] || [ -n "$CUSTOM_
|
|
| 206 |
CUSTOM_PROVIDER_OK=true
|
| 207 |
|
| 208 |
if [ -z "$CUSTOM_PROVIDER_NAME" ] || [ -z "$CUSTOM_BASE_URL" ] || [ -z "$CUSTOM_MODEL_ID" ]; then
|
| 209 |
-
echo "
|
| 210 |
CUSTOM_PROVIDER_OK=false
|
| 211 |
fi
|
| 212 |
|
| 213 |
case "$CUSTOM_PROVIDER_NORMALIZED" in
|
| 214 |
anthropic|openai|openai-codex|google|google-vertex|deepseek|opencode|opencode-go|openrouter|kilocode|vercel-ai-gateway|zai|z-ai|z.ai|zhipu|moonshot|kimi-coding|minimax|qwen|modelstudio|xiaomi|volcengine|volcengine-plan|byteplus|byteplus-plan|qianfan|mistral|mistralai|xai|x-ai|nvidia|cohere|groq|together|huggingface|cerebras|venice|synthetic|github-copilot)
|
| 215 |
-
echo "
|
| 216 |
CUSTOM_PROVIDER_OK=false
|
| 217 |
;;
|
| 218 |
esac
|
| 219 |
|
| 220 |
if [[ "$CUSTOM_BASE_URL_NORMALIZED" == */chat/completions ]] || [[ "$CUSTOM_BASE_URL_NORMALIZED" == */completions ]]; then
|
| 221 |
-
echo "
|
| 222 |
CUSTOM_PROVIDER_OK=false
|
| 223 |
fi
|
| 224 |
|
| 225 |
if ! [[ "$CUSTOM_CONTEXT_WINDOW" =~ ^[0-9]+$ ]] || ! [[ "$CUSTOM_MAX_TOKENS" =~ ^[0-9]+$ ]]; then
|
| 226 |
-
echo "
|
| 227 |
CUSTOM_PROVIDER_OK=false
|
| 228 |
fi
|
| 229 |
|
| 230 |
if [ "$CUSTOM_PROVIDER_OK" = "true" ]; then
|
| 231 |
-
echo "
|
| 232 |
CONFIG_JSON=$(jq \
|
| 233 |
--arg provider "$CUSTOM_PROVIDER_NAME" \
|
| 234 |
--arg baseUrl "$CUSTOM_BASE_URL_NORMALIZED" \
|
|
@@ -252,7 +262,7 @@ if [ -n "$CUSTOM_PROVIDER_NAME" ] || [ -n "$CUSTOM_BASE_URL" ] || [ -n "$CUSTOM_
|
|
| 252 |
}' <<<"$CONFIG_JSON")
|
| 253 |
|
| 254 |
if [[ "$LLM_MODEL" != "$CUSTOM_PROVIDER_NAME/"* ]]; then
|
| 255 |
-
echo "
|
| 256 |
fi
|
| 257 |
fi
|
| 258 |
fi
|
|
@@ -273,7 +283,15 @@ elif [ "$BROWSER_PLUGIN_MODE" = "auto" ] && [ -n "$BROWSER_EXECUTABLE_PATH" ] &&
|
|
| 273 |
BROWSER_SHOULD_ENABLE=true
|
| 274 |
fi
|
| 275 |
|
| 276 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 277 |
PLUGIN_ALLOW_JSON='["device-pair","phone-control","talk-voice"]'
|
| 278 |
if [ "$ACP_PLUGIN_MODE" = "enabled" ] || [ "$ACP_PLUGIN_MODE" = "auto" ]; then
|
| 279 |
PLUGIN_ALLOW_JSON=$(jq '. + ["acpx"]' <<<"$PLUGIN_ALLOW_JSON")
|
|
@@ -287,41 +305,52 @@ fi
|
|
| 287 |
if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
|
| 288 |
PLUGIN_ALLOW_JSON=$(jq '. + ["whatsapp"]' <<<"$PLUGIN_ALLOW_JSON")
|
| 289 |
fi
|
| 290 |
-
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq ".plugins.allow = $PLUGIN_ALLOW_JSON")
|
| 291 |
-
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.deny = ["lmstudio","xai"]')
|
| 292 |
-
CONFIG_JSON=$(echo "$CONFIG_JSON" | jq '.plugins.entries.lmstudio.enabled = false | .plugins.entries.xai.enabled = false')
|
| 293 |
|
| 294 |
-
|
| 295 |
-
|
| 296 |
-
fi
|
| 297 |
-
|
| 298 |
-
if [ "$BROWSER_SHOULD_ENABLE"
|
| 299 |
-
|
| 300 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 301 |
|
| 302 |
if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then
|
| 303 |
-
CONFIG_JSON=$(
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
|
| 316 |
-
|
| 317 |
-
|
| 318 |
-
|
| 319 |
-
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
if
|
| 323 |
-
|
| 324 |
-
|
|
|
|
|
|
|
|
|
|
| 325 |
|
| 326 |
# Trusted proxies (optional β fixes "Proxy headers detected from untrusted address" on HF Spaces)
|
| 327 |
# Set TRUSTED_PROXIES as comma-separated IPs/CIDRs, e.g. "10.20.31.87,10.20.26.157"
|
|
@@ -365,12 +394,16 @@ if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
|
|
| 365 |
')
|
| 366 |
|
| 367 |
if [ -n "${TELEGRAM_USER_IDS:-}" ]; then
|
| 368 |
-
# Convert comma-separated IDs to JSON array
|
| 369 |
IDS_JSON=$(echo "$TELEGRAM_USER_IDS" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -s .)
|
| 370 |
-
CONFIG_JSON=$(
|
|
|
|
|
|
|
| 371 |
elif [ -n "${TELEGRAM_USER_ID:-}" ]; then
|
| 372 |
-
# Single user (backward compatible)
|
| 373 |
-
CONFIG_JSON=$(
|
|
|
|
|
|
|
| 374 |
fi
|
| 375 |
fi
|
| 376 |
|
|
@@ -445,13 +478,13 @@ warmup_browser() {
|
|
| 445 |
for attempt in 1 2 3 4 5; do
|
| 446 |
if openclaw browser --browser-profile openclaw start >/dev/null 2>&1; then
|
| 447 |
openclaw browser --browser-profile openclaw open about:blank >/dev/null 2>&1 || true
|
| 448 |
-
echo "
|
| 449 |
return 0
|
| 450 |
fi
|
| 451 |
sleep 2
|
| 452 |
done
|
| 453 |
|
| 454 |
-
echo "
|
| 455 |
) &
|
| 456 |
}
|
| 457 |
|
|
@@ -470,15 +503,32 @@ if [ "${GATEWAY_VERBOSE:-0}" = "1" ]; then
|
|
| 470 |
echo "Gateway verbose logging enabled (GATEWAY_VERBOSE=1)"
|
| 471 |
fi
|
| 472 |
|
| 473 |
-
# Use stdbuf -oL -eL to ensure logs are not buffered and appear immediately
|
|
|
|
|
|
|
|
|
|
| 474 |
stdbuf -oL -eL openclaw "${GATEWAY_ARGS[@]}" 2>&1 | tee -a /home/node/.openclaw/gateway.log &
|
| 475 |
GATEWAY_PID=$!
|
| 476 |
|
| 477 |
-
#
|
| 478 |
-
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 480 |
echo ""
|
| 481 |
-
echo "
|
| 482 |
echo "ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 483 |
tail -30 /home/node/.openclaw/gateway.log
|
| 484 |
exit 1
|
|
|
|
| 38 |
# ββ Validate required secrets ββ
|
| 39 |
ERRORS=""
|
| 40 |
if [ -z "$LLM_API_KEY" ]; then
|
| 41 |
+
ERRORS="${ERRORS} - LLM_API_KEY is not set\n"
|
| 42 |
fi
|
| 43 |
if [ -z "$LLM_MODEL" ]; then
|
| 44 |
+
ERRORS="${ERRORS} - LLM_MODEL is not set (e.g. google/gemini-2.5-flash, anthropic/claude-sonnet-4-5, openai/gpt-4)\n"
|
| 45 |
fi
|
| 46 |
if [ -z "$GATEWAY_TOKEN" ]; then
|
| 47 |
+
ERRORS="${ERRORS} - GATEWAY_TOKEN is not set (generate: openssl rand -hex 32)\n"
|
| 48 |
fi
|
| 49 |
if [ -n "$ERRORS" ]; then
|
| 50 |
echo "Missing required secrets:"
|
|
|
|
| 73 |
# Auto-correct Gemini models to use google/ prefix if anthropic/ was mistakenly used
|
| 74 |
if [[ "$LLM_MODEL" == "anthropic/gemini"* ]]; then
|
| 75 |
LLM_MODEL=$(echo "$LLM_MODEL" | sed 's/^anthropic\//google\//')
|
| 76 |
+
echo "Note: corrected model from anthropic/gemini* to google/gemini*"
|
| 77 |
fi
|
| 78 |
|
| 79 |
# Extract provider prefix from model name (e.g. "google/gemini-2.5-flash" β "google")
|
|
|
|
| 146 |
CF_PROXY_ENV_FILE="/tmp/huggingclaw-cloudflare-proxy.env"
|
| 147 |
if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ] || [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
|
| 148 |
export CLOUDFLARE_PROXY_DOMAINS="${CLOUDFLARE_PROXY_DOMAINS:-api.telegram.org,web.whatsapp.com,googleapis.com}"
|
| 149 |
+
# Default debug off for production. Set CLOUDFLARE_PROXY_DEBUG=true in HF
|
| 150 |
+
# Space secrets to surface per-request "Redirecting" + error-cause logs.
|
| 151 |
+
export CLOUDFLARE_PROXY_DEBUG="${CLOUDFLARE_PROXY_DEBUG:-false}"
|
| 152 |
echo "Preparing Cloudflare outbound proxy..."
|
| 153 |
python3 /home/node/app/cloudflare-proxy-setup.py || true
|
| 154 |
if [ -f "$CF_PROXY_ENV_FILE" ]; then
|
|
|
|
| 185 |
CONFIGEOF
|
| 186 |
)
|
| 187 |
|
| 188 |
+
# Apply gateway token, model, and logging in a single jq pass.
|
| 189 |
+
# Uses --arg so values containing quotes/backslashes can't break the JSON or
|
| 190 |
+
# inject jq filters (relevant for OPENCLAW_PASSWORD/GATEWAY_TOKEN below too).
|
| 191 |
+
CONFIG_JSON=$(jq \
|
| 192 |
+
--arg token "$GATEWAY_TOKEN" \
|
| 193 |
+
--arg model "$LLM_MODEL" \
|
| 194 |
+
--arg fileLevel "$OPENCLAW_FILE_LOG_LEVEL" \
|
| 195 |
+
--arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
|
| 196 |
+
--arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
|
| 197 |
+
'.gateway.auth.token = $token
|
| 198 |
+
| .agents.defaults.model = $model
|
| 199 |
+
| .logging.level = $fileLevel
|
| 200 |
+
| .logging.consoleLevel = $consoleLevel
|
| 201 |
+
| .logging.consoleStyle = $consoleStyle' <<<"$CONFIG_JSON")
|
| 202 |
|
| 203 |
# Optional: dynamic custom OpenAI-compatible provider registration
|
| 204 |
CUSTOM_PROVIDER_NAME="${CUSTOM_PROVIDER_NAME:-}"
|
|
|
|
| 216 |
CUSTOM_PROVIDER_OK=true
|
| 217 |
|
| 218 |
if [ -z "$CUSTOM_PROVIDER_NAME" ] || [ -z "$CUSTOM_BASE_URL" ] || [ -z "$CUSTOM_MODEL_ID" ]; then
|
| 219 |
+
echo "Warning: custom provider skipped: set CUSTOM_PROVIDER_NAME, CUSTOM_BASE_URL, and CUSTOM_MODEL_ID together."
|
| 220 |
CUSTOM_PROVIDER_OK=false
|
| 221 |
fi
|
| 222 |
|
| 223 |
case "$CUSTOM_PROVIDER_NORMALIZED" in
|
| 224 |
anthropic|openai|openai-codex|google|google-vertex|deepseek|opencode|opencode-go|openrouter|kilocode|vercel-ai-gateway|zai|z-ai|z.ai|zhipu|moonshot|kimi-coding|minimax|qwen|modelstudio|xiaomi|volcengine|volcengine-plan|byteplus|byteplus-plan|qianfan|mistral|mistralai|xai|x-ai|nvidia|cohere|groq|together|huggingface|cerebras|venice|synthetic|github-copilot)
|
| 225 |
+
echo "Warning: custom provider skipped: CUSTOM_PROVIDER_NAME='$CUSTOM_PROVIDER_NAME' conflicts with a built-in provider."
|
| 226 |
CUSTOM_PROVIDER_OK=false
|
| 227 |
;;
|
| 228 |
esac
|
| 229 |
|
| 230 |
if [[ "$CUSTOM_BASE_URL_NORMALIZED" == */chat/completions ]] || [[ "$CUSTOM_BASE_URL_NORMALIZED" == */completions ]]; then
|
| 231 |
+
echo "Warning: custom provider skipped: CUSTOM_BASE_URL should be the API base URL, not a completions endpoint."
|
| 232 |
CUSTOM_PROVIDER_OK=false
|
| 233 |
fi
|
| 234 |
|
| 235 |
if ! [[ "$CUSTOM_CONTEXT_WINDOW" =~ ^[0-9]+$ ]] || ! [[ "$CUSTOM_MAX_TOKENS" =~ ^[0-9]+$ ]]; then
|
| 236 |
+
echo "Warning: custom provider skipped: CUSTOM_CONTEXT_WINDOW and CUSTOM_MAX_TOKENS must be whole numbers."
|
| 237 |
CUSTOM_PROVIDER_OK=false
|
| 238 |
fi
|
| 239 |
|
| 240 |
if [ "$CUSTOM_PROVIDER_OK" = "true" ]; then
|
| 241 |
+
echo "Registering custom provider: $CUSTOM_PROVIDER_NAME -> $CUSTOM_BASE_URL_NORMALIZED"
|
| 242 |
CONFIG_JSON=$(jq \
|
| 243 |
--arg provider "$CUSTOM_PROVIDER_NAME" \
|
| 244 |
--arg baseUrl "$CUSTOM_BASE_URL_NORMALIZED" \
|
|
|
|
| 262 |
}' <<<"$CONFIG_JSON")
|
| 263 |
|
| 264 |
if [[ "$LLM_MODEL" != "$CUSTOM_PROVIDER_NAME/"* ]]; then
|
| 265 |
+
echo "Warning: custom provider registered, but LLM_MODEL='$LLM_MODEL' does not start with '$CUSTOM_PROVIDER_NAME/'."
|
| 266 |
fi
|
| 267 |
fi
|
| 268 |
fi
|
|
|
|
| 283 |
BROWSER_SHOULD_ENABLE=true
|
| 284 |
fi
|
| 285 |
|
| 286 |
+
# Plugin allow/deny rationale:
|
| 287 |
+
# ALLOW: device-pair, phone-control, talk-voice are the minimum bundled
|
| 288 |
+
# plugins that the Control UI/dashboard needs to render correctly
|
| 289 |
+
# on HF Spaces. Without these the UI shows blank panels.
|
| 290 |
+
# telegram/whatsapp/browser/acpx are added conditionally below.
|
| 291 |
+
# DENY: lmstudio crashes on boot when no local server is reachable;
|
| 292 |
+
# xai PLUGIN (separate from the xai model PROVIDER) is broken in
|
| 293 |
+
# current OpenClaw releases and prevents gateway start. Disabling
|
| 294 |
+
# the plugin does NOT affect xai-as-a-model-provider.
|
| 295 |
PLUGIN_ALLOW_JSON='["device-pair","phone-control","talk-voice"]'
|
| 296 |
if [ "$ACP_PLUGIN_MODE" = "enabled" ] || [ "$ACP_PLUGIN_MODE" = "auto" ]; then
|
| 297 |
PLUGIN_ALLOW_JSON=$(jq '. + ["acpx"]' <<<"$PLUGIN_ALLOW_JSON")
|
|
|
|
| 305 |
if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
|
| 306 |
PLUGIN_ALLOW_JSON=$(jq '. + ["whatsapp"]' <<<"$PLUGIN_ALLOW_JSON")
|
| 307 |
fi
|
|
|
|
|
|
|
|
|
|
| 308 |
|
| 309 |
+
# Apply plugin allow/deny + per-entry toggles in one jq pass.
|
| 310 |
+
ACPX_DISABLED=false
|
| 311 |
+
if [ "$ACP_PLUGIN_MODE" = "disabled" ]; then ACPX_DISABLED=true; fi
|
| 312 |
+
BROWSER_DISABLED=true
|
| 313 |
+
if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then BROWSER_DISABLED=false; fi
|
| 314 |
+
|
| 315 |
+
CONFIG_JSON=$(jq \
|
| 316 |
+
--argjson allow "$PLUGIN_ALLOW_JSON" \
|
| 317 |
+
--argjson acpxDisabled "$ACPX_DISABLED" \
|
| 318 |
+
--argjson browserDisabled "$BROWSER_DISABLED" \
|
| 319 |
+
'.plugins.allow = $allow
|
| 320 |
+
| .plugins.deny = ["lmstudio","xai"]
|
| 321 |
+
| .plugins.entries.lmstudio.enabled = false
|
| 322 |
+
| .plugins.entries.xai.enabled = false
|
| 323 |
+
| (if $acpxDisabled then .plugins.entries.acpx.enabled = false else . end)
|
| 324 |
+
| (if $browserDisabled then
|
| 325 |
+
.plugins.entries.browser.enabled = false | .browser.enabled = false
|
| 326 |
+
else . end)' <<<"$CONFIG_JSON")
|
| 327 |
|
| 328 |
if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then
|
| 329 |
+
CONFIG_JSON=$(jq \
|
| 330 |
+
--arg execPath "$BROWSER_EXECUTABLE_PATH" \
|
| 331 |
+
'.browser = {
|
| 332 |
+
"enabled": true,
|
| 333 |
+
"defaultProfile": "openclaw",
|
| 334 |
+
"headless": true,
|
| 335 |
+
"noSandbox": true,
|
| 336 |
+
"executablePath": $execPath
|
| 337 |
+
}
|
| 338 |
+
| .agents.defaults.sandbox.browser.allowHostControl = true' <<<"$CONFIG_JSON")
|
| 339 |
+
fi
|
| 340 |
+
|
| 341 |
+
# Control UI origin (allow HF Space URL for web UI access).
|
| 342 |
+
# Disable device auth (pairing) for headless Docker β token-only auth.
|
| 343 |
+
# Combined into one jq pass; --arg keeps password/host injection-safe.
|
| 344 |
+
CONFIG_JSON=$(jq \
|
| 345 |
+
--arg spaceHost "${SPACE_HOST:-}" \
|
| 346 |
+
--arg password "${OPENCLAW_PASSWORD:-}" \
|
| 347 |
+
'.gateway.controlUi.dangerouslyDisableDeviceAuth = true
|
| 348 |
+
| (if $spaceHost != "" then
|
| 349 |
+
.gateway.controlUi.allowedOrigins = ["https://" + $spaceHost]
|
| 350 |
+
else . end)
|
| 351 |
+
| (if $password != "" then
|
| 352 |
+
.gateway.auth.mode = "password" | .gateway.auth.password = $password
|
| 353 |
+
else . end)' <<<"$CONFIG_JSON")
|
| 354 |
|
| 355 |
# Trusted proxies (optional β fixes "Proxy headers detected from untrusted address" on HF Spaces)
|
| 356 |
# Set TRUSTED_PROXIES as comma-separated IPs/CIDRs, e.g. "10.20.31.87,10.20.26.157"
|
|
|
|
| 394 |
')
|
| 395 |
|
| 396 |
if [ -n "${TELEGRAM_USER_IDS:-}" ]; then
|
| 397 |
+
# Convert comma-separated IDs to JSON array (already safe β jq -R parses).
|
| 398 |
IDS_JSON=$(echo "$TELEGRAM_USER_IDS" | tr ',' '\n' | sed 's/^ *//;s/ *$//' | jq -R . | jq -s .)
|
| 399 |
+
CONFIG_JSON=$(jq \
|
| 400 |
+
--argjson ids "$IDS_JSON" \
|
| 401 |
+
'.channels.telegram += {"dmPolicy": "allowlist", "allowFrom": $ids}' <<<"$CONFIG_JSON")
|
| 402 |
elif [ -n "${TELEGRAM_USER_ID:-}" ]; then
|
| 403 |
+
# Single user (backward compatible). --arg keeps quotes/odd chars safe.
|
| 404 |
+
CONFIG_JSON=$(jq \
|
| 405 |
+
--arg userId "$TELEGRAM_USER_ID" \
|
| 406 |
+
'.channels.telegram += {"dmPolicy": "allowlist", "allowFrom": [$userId]}' <<<"$CONFIG_JSON")
|
| 407 |
fi
|
| 408 |
fi
|
| 409 |
|
|
|
|
| 478 |
for attempt in 1 2 3 4 5; do
|
| 479 |
if openclaw browser --browser-profile openclaw start >/dev/null 2>&1; then
|
| 480 |
openclaw browser --browser-profile openclaw open about:blank >/dev/null 2>&1 || true
|
| 481 |
+
echo "Managed browser ready."
|
| 482 |
return 0
|
| 483 |
fi
|
| 484 |
sleep 2
|
| 485 |
done
|
| 486 |
|
| 487 |
+
echo "Warning: managed browser warm-up did not complete; first browser action may need a retry."
|
| 488 |
) &
|
| 489 |
}
|
| 490 |
|
|
|
|
| 503 |
echo "Gateway verbose logging enabled (GATEWAY_VERBOSE=1)"
|
| 504 |
fi
|
| 505 |
|
| 506 |
+
# Use stdbuf -oL -eL to ensure logs are not buffered and appear immediately
|
| 507 |
+
# in the console. NOTE: $! captures the LAST pipeline element (tee), not
|
| 508 |
+
# openclaw β fine for passing to `wait` (waits for the whole pipeline to
|
| 509 |
+
# finish), but kill -0 on it is uninformative. We probe TCP instead.
|
| 510 |
stdbuf -oL -eL openclaw "${GATEWAY_ARGS[@]}" 2>&1 | tee -a /home/node/.openclaw/gateway.log &
|
| 511 |
GATEWAY_PID=$!
|
| 512 |
|
| 513 |
+
# Poll for the gateway to start listening on 7860. OpenClaw can take 20-30s
|
| 514 |
+
# on cold start (plugin install + auto-restore). Bail out early if the
|
| 515 |
+
# pipeline died.
|
| 516 |
+
GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-90}"
|
| 517 |
+
ready=false
|
| 518 |
+
for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do
|
| 519 |
+
if (echo > /dev/tcp/127.0.0.1/7860) 2>/dev/null; then
|
| 520 |
+
ready=true
|
| 521 |
+
break
|
| 522 |
+
fi
|
| 523 |
+
if ! kill -0 "$GATEWAY_PID" 2>/dev/null; then
|
| 524 |
+
break
|
| 525 |
+
fi
|
| 526 |
+
sleep 1
|
| 527 |
+
done
|
| 528 |
+
|
| 529 |
+
if [ "$ready" != "true" ]; then
|
| 530 |
echo ""
|
| 531 |
+
echo "Gateway failed to start. Last 30 lines of log:"
|
| 532 |
echo "ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 533 |
tail -30 /home/node/.openclaw/gateway.log
|
| 534 |
exit 1
|
|
@@ -161,7 +161,17 @@ def restore_embedded_state() -> None:
|
|
| 161 |
shutil.rmtree(WHATSAPP_CREDS_DIR, ignore_errors=True)
|
| 162 |
WHATSAPP_CREDS_DIR.parent.mkdir(parents=True, exist_ok=True)
|
| 163 |
shutil.copytree(WHATSAPP_BACKUP_DIR, WHATSAPP_CREDS_DIR)
|
|
|
|
|
|
|
| 164 |
os.chmod(OPENCLAW_HOME / "credentials", 0o700)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 165 |
print("WhatsApp credentials restored.")
|
| 166 |
else:
|
| 167 |
print(f"Warning: saved WhatsApp credentials incomplete ({file_count} files), skipping restore.")
|
|
|
|
| 161 |
shutil.rmtree(WHATSAPP_CREDS_DIR, ignore_errors=True)
|
| 162 |
WHATSAPP_CREDS_DIR.parent.mkdir(parents=True, exist_ok=True)
|
| 163 |
shutil.copytree(WHATSAPP_BACKUP_DIR, WHATSAPP_CREDS_DIR)
|
| 164 |
+
# Lock down dir tree: 0700 on directories, 0600 on every file
|
| 165 |
+
# so the WhatsApp session secrets can't be read by other users.
|
| 166 |
os.chmod(OPENCLAW_HOME / "credentials", 0o700)
|
| 167 |
+
for path in WHATSAPP_CREDS_DIR.rglob("*"):
|
| 168 |
+
try:
|
| 169 |
+
if path.is_dir():
|
| 170 |
+
os.chmod(path, 0o700)
|
| 171 |
+
elif path.is_file():
|
| 172 |
+
os.chmod(path, 0o600)
|
| 173 |
+
except OSError:
|
| 174 |
+
pass
|
| 175 |
print("WhatsApp credentials restored.")
|
| 176 |
else:
|
| 177 |
print(f"Warning: saved WhatsApp credentials incomplete ({file_count} files), skipping restore.")
|