Claude commited on
Commit
336f8ec
Β·
1 Parent(s): c952f5b

fix: IPv4 forcing, proxy fallback, browser symlink noise, model injection, WA stall recovery

Browse files

- start.sh: force IPv4 globally for all outbound connections on HF Spaces
(fixes WhatsApp status 428 and Telegram fetch failures caused by IPv6)
- start.sh: clean up stale browser-automation plugin-skills symlinks on
startup to suppress repeated 'not a generated symlink' log noise
- start.sh: fix Telegram config patch overwriting stale env-driven fields
on reboot; now always re-applies all env-driven fields
- start.sh: fix nvidia double-prefix in model catalog, vertex if-guard,
vertex pool-key; fix model injection for multi-key pools, custom
provider baseUrl, and Vertex AI
- cloudflare-proxy.js: replace logProxyError (log-only) with
proxyWithFallback β€” on hard network failure (ETIMEDOUT/ECONNRESET)
automatically retries the request direct, bypassing the proxy; fixes
'Proxy FAILED raw.githubusercontent.com: ETIMEDOUT' and ensures update
checks / plugin installs always succeed
- env-builder.js: related env-builder improvements shipped alongside above

Files changed (3) hide show
  1. cloudflare-proxy.js +29 -20
  2. env-builder.js +47 -0
  3. start.sh +184 -42
cloudflare-proxy.js CHANGED
@@ -204,24 +204,31 @@ if (PROXY_URL) {
204
 
205
  const proxiedUrl = new URL(url.pathname + url.search, proxy);
206
 
207
- const logProxyError = (promise, debugInfo) => {
208
- promise
209
- .then(r => {
210
- if (DEBUG && !r.ok) {
211
- log(`[cloudflare-proxy] Proxy HTTP ${r.status} for ${hostname}: ${r.statusText}`);
212
- }
213
- })
214
- .catch(err => {
215
- const cause = err?.cause;
216
- const causeStr = cause
217
- ? ` | cause: ${cause?.code || cause?.message || String(cause)}`
218
- : "";
219
- log(`[cloudflare-proxy] Proxy FAILED ${hostname}: ${err?.message}${causeStr}`);
220
- if (DEBUG && debugInfo) {
221
- log(`[cloudflare-proxy] Debug: ${debugInfo}`);
222
- }
223
- });
224
- return promise;
 
 
 
 
 
 
 
225
  };
226
 
227
  if (request) {
@@ -234,8 +241,9 @@ if (PROXY_URL) {
234
  fetchOpts.body = request.body;
235
  fetchOpts.duplex = request.duplex || "half";
236
  }
237
- return logProxyError(
238
  originalFetch(String(proxiedUrl), fetchOpts),
 
239
  `request-mode method=${request.method} hasBody=${!!request.body}`,
240
  );
241
  }
@@ -270,8 +278,9 @@ if (PROXY_URL) {
270
  ? "ReadableStream"
271
  : (init.body?.constructor?.name || typeof init.body);
272
 
273
- return logProxyError(
274
  originalFetch(String(proxiedUrl), newInit),
 
275
  `init-mode method=${newInit.method} body=${bodyType} initKeys=${Object.keys(init || {}).join(",")}`,
276
  );
277
  };
 
204
 
205
  const proxiedUrl = new URL(url.pathname + url.search, proxy);
206
 
207
+ // proxyWithFallback: try via Cloudflare Worker first; if the proxy
208
+ // itself hard-fails (ETIMEDOUT / ECONNRESET / network error), fall
209
+ // back to a direct connection so callers still get a response.
210
+ // HTTP-level errors from the Worker (4xx/5xx) are NOT retried β€”
211
+ // only hard network failures (rejected promise) trigger the fallback.
212
+ const proxyWithFallback = (proxyPromise, directFallbackFn, debugInfo) => {
213
+ return proxyPromise.then(r => {
214
+ if (DEBUG && !r.ok) {
215
+ log(`[cloudflare-proxy] Proxy HTTP ${r.status} for ${hostname}: ${r.statusText}`);
216
+ }
217
+ return r;
218
+ }).catch(err => {
219
+ const cause = err?.cause;
220
+ const causeStr = cause
221
+ ? ` | cause: ${cause?.code || cause?.message || String(cause)}`
222
+ : "";
223
+ log(`[cloudflare-proxy] Proxy FAILED ${hostname}: ${err?.message}${causeStr} β€” retrying direct`);
224
+ if (DEBUG && debugInfo) {
225
+ log(`[cloudflare-proxy] Debug: ${debugInfo}`);
226
+ }
227
+ // Direct fallback: bypasses proxy for this request so infrastructure
228
+ // calls (update checks, plugin installs, etc.) still succeed even
229
+ // when the Worker cannot reach the destination.
230
+ return directFallbackFn();
231
+ });
232
  };
233
 
234
  if (request) {
 
241
  fetchOpts.body = request.body;
242
  fetchOpts.duplex = request.duplex || "half";
243
  }
244
+ return proxyWithFallback(
245
  originalFetch(String(proxiedUrl), fetchOpts),
246
+ () => originalFetch(input, init),
247
  `request-mode method=${request.method} hasBody=${!!request.body}`,
248
  );
249
  }
 
278
  ? "ReadableStream"
279
  : (init.body?.constructor?.name || typeof init.body);
280
 
281
+ return proxyWithFallback(
282
  originalFetch(String(proxiedUrl), newInit),
283
+ () => originalFetch(input, init),
284
  `init-mode method=${newInit.method} body=${bodyType} initKeys=${Object.keys(init || {}).join(",")}`,
285
  );
286
  };
env-builder.js CHANGED
@@ -251,6 +251,13 @@ const MODEL_CATALOGS = {
251
  "google/gemini-2.5-flash",
252
  "google/gemini-2.0-flash"
253
  ],
 
 
 
 
 
 
 
254
  "DEEPSEEK_MODELS": [
255
  "deepseek/deepseek-v4-pro",
256
  "deepseek/deepseek-v4-flash",
@@ -1108,6 +1115,36 @@ const FIELDS = [
1108
  "common": 0,
1109
  "tag": "credential"
1110
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1111
  {
1112
  "g": "Provider Keys",
1113
  "icon": "πŸ”‘",
@@ -1530,6 +1567,16 @@ const FIELDS = [
1530
  "ph": "Select models to build a comma list",
1531
  "tag": "optional"
1532
  },
 
 
 
 
 
 
 
 
 
 
1533
  {
1534
  "g": "Model Lists",
1535
  "icon": "πŸ“‹",
 
251
  "google/gemini-2.5-flash",
252
  "google/gemini-2.0-flash"
253
  ],
254
+ "VERTEX_MODELS": [
255
+ "google-vertex/gemini-2.5-pro",
256
+ "google-vertex/gemini-2.5-flash",
257
+ "google-vertex/gemini-2.0-flash",
258
+ "google-vertex/gemini-1.5-pro",
259
+ "google-vertex/gemini-1.5-flash"
260
+ ],
261
  "DEEPSEEK_MODELS": [
262
  "deepseek/deepseek-v4-pro",
263
  "deepseek/deepseek-v4-flash",
 
1115
  "common": 0,
1116
  "tag": "credential"
1117
  },
1118
+ {
1119
+ "g": "Provider Keys",
1120
+ "icon": "πŸ”‘",
1121
+ "k": "GOOGLE_CLOUD_PROJECT",
1122
+ "lbl": "Google Vertex AI β€” GCP Project ID",
1123
+ "type": "text",
1124
+ "ph": "my-gcp-project-id",
1125
+ "common": 0,
1126
+ "tag": "credential"
1127
+ },
1128
+ {
1129
+ "g": "Provider Keys",
1130
+ "icon": "πŸ”‘",
1131
+ "k": "GOOGLE_CLOUD_LOCATION",
1132
+ "lbl": "Google Vertex AI β€” GCP Region",
1133
+ "type": "text",
1134
+ "ph": "us-central1",
1135
+ "common": 0,
1136
+ "tag": "credential"
1137
+ },
1138
+ {
1139
+ "g": "Provider Keys",
1140
+ "icon": "πŸ”‘",
1141
+ "k": "GOOGLE_APPLICATION_CREDENTIALS_JSON",
1142
+ "lbl": "Google Vertex AI β€” Service Account JSON (base64)",
1143
+ "type": "password",
1144
+ "ph": "base64-encoded service account JSON",
1145
+ "common": 0,
1146
+ "tag": "credential"
1147
+ },
1148
  {
1149
  "g": "Provider Keys",
1150
  "icon": "πŸ”‘",
 
1567
  "ph": "Select models to build a comma list",
1568
  "tag": "optional"
1569
  },
1570
+ {
1571
+ "g": "Model Lists",
1572
+ "icon": "πŸ“‹",
1573
+ "k": "VERTEX_MODELS",
1574
+ "lbl": "Visible Vertex AI models (google-vertex/...)",
1575
+ "type": "model_list",
1576
+ "options_key": "VERTEX_MODELS",
1577
+ "ph": "Select Vertex models (needs GOOGLE_CLOUD_PROJECT + GOOGLE_CLOUD_LOCATION)",
1578
+ "tag": "optional"
1579
+ },
1580
  {
1581
  "g": "Model Lists",
1582
  "icon": "πŸ“‹",
start.sh CHANGED
@@ -124,6 +124,15 @@ if [ -n "${SPACE_HOST:-}" ]; then
124
  ACP_PLUGIN_MODE="${ACP_PLUGIN_MODE:-disabled}"
125
  # HF Spaces does not benefit from Bonjour discovery, and the retries add noise.
126
  export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-1}"
 
 
 
 
 
 
 
 
 
127
  else
128
  OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-info}"
129
  OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
@@ -454,20 +463,54 @@ fi
454
  # NVIDIA_MODELS=model1,model2
455
  # OPENAI_MODELS=gpt-4o-mini,gpt-4.1
456
  # This helps when provider auto-discovery does not populate models reliably.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  INJECTED_MODELS_PROVIDERS='{}'
458
  inject_provider_models_from_env() {
459
  local provider="$1"
460
  local models_env="$2"
461
  local key_env_single="$3"
462
  local key_env_pool="$4"
 
463
  local models_csv="${!models_env:-}"
464
  local single_key="${!key_env_single:-}"
465
  local pool_keys="${!key_env_pool:-}"
466
 
467
- # Only inject when both:
468
- # 1) provider has at least one configured key
469
- # 2) explicit model list env is provided
470
- if [ -z "$models_csv" ] || { [ -z "$single_key" ] && [ -z "$pool_keys" ]; }; then
 
 
 
 
 
 
 
 
471
  return 0
472
  fi
473
 
@@ -478,7 +521,18 @@ inject_provider_models_from_env() {
478
  | awk 'NF' \
479
  | jq -R . \
480
  | jq -s --arg provider "$provider" '
481
- map(if contains("/") then . else ($provider + "/" + .) end)
 
 
 
 
 
 
 
 
 
 
 
482
  | map({id: ., name: .})
483
  | unique_by(.id)')
484
 
@@ -494,45 +548,64 @@ inject_provider_models_from_env() {
494
  '.[$provider] = ((.[$provider] // {}) + {models: $models})' <<<"$INJECTED_MODELS_PROVIDERS")
495
  }
496
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
497
  # Built-in provider model envs (optional)
498
- inject_provider_models_from_env "anthropic" "ANTHROPIC_MODELS" "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS"
499
- inject_provider_models_from_env "openai" "OPENAI_MODELS" "OPENAI_API_KEY" "OPENAI_API_KEYS"
500
- inject_provider_models_from_env "openai-codex" "OPENAI_MODELS" "OPENAI_API_KEY" "OPENAI_API_KEYS"
501
- inject_provider_models_from_env "google" "GEMINI_MODELS" "GEMINI_API_KEY" "GEMINI_API_KEYS"
502
- inject_provider_models_from_env "google-vertex" "GEMINI_MODELS" "GEMINI_API_KEY" "GEMINI_API_KEYS"
503
- inject_provider_models_from_env "deepseek" "DEEPSEEK_MODELS" "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS"
504
- inject_provider_models_from_env "openrouter" "OPENROUTER_MODELS" "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS"
505
- inject_provider_models_from_env "kilocode" "KILOCODE_MODELS" "KILOCODE_API_KEY" "KILOCODE_API_KEYS"
506
- inject_provider_models_from_env "opencode" "OPENCODE_MODELS" "OPENCODE_API_KEY" "OPENCODE_API_KEYS"
507
- inject_provider_models_from_env "opencode-go" "OPENCODE_MODELS" "OPENCODE_API_KEY" "OPENCODE_API_KEYS"
508
- inject_provider_models_from_env "zai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS"
509
- inject_provider_models_from_env "z-ai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS"
510
- inject_provider_models_from_env "z.ai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS"
511
- inject_provider_models_from_env "zhipu" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS"
512
- inject_provider_models_from_env "moonshot" "MOONSHOT_MODELS" "MOONSHOT_API_KEY" "MOONSHOT_API_KEYS"
 
 
 
 
 
513
  inject_provider_models_from_env "kimi-coding" "KIMI_MODELS" "KIMI_API_KEY" "KIMI_API_KEYS"
514
- inject_provider_models_from_env "minimax" "MINIMAX_MODELS" "MINIMAX_API_KEY" "MINIMAX_API_KEYS"
515
- inject_provider_models_from_env "modelstudio" "MODELSTUDIO_MODELS" "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS"
516
- inject_provider_models_from_env "qwen" "MODELSTUDIO_MODELS" "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS"
517
  inject_provider_models_from_env "xiaomi" "XIAOMI_MODELS" "XIAOMI_API_KEY" "XIAOMI_API_KEYS"
518
  inject_provider_models_from_env "volcengine" "VOLCANO_ENGINE_MODELS" "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS"
519
  inject_provider_models_from_env "volcengine-plan" "VOLCANO_ENGINE_MODELS" "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS"
520
  inject_provider_models_from_env "byteplus" "BYTEPLUS_MODELS" "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS"
521
  inject_provider_models_from_env "byteplus-plan" "BYTEPLUS_MODELS" "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS"
522
  inject_provider_models_from_env "qianfan" "QIANFAN_MODELS" "QIANFAN_API_KEY" "QIANFAN_API_KEYS"
523
- inject_provider_models_from_env "groq" "GROQ_MODELS" "GROQ_API_KEY" "GROQ_API_KEYS"
524
- inject_provider_models_from_env "mistral" "MISTRAL_MODELS" "MISTRAL_API_KEY" "MISTRAL_API_KEYS"
525
- inject_provider_models_from_env "mistralai" "MISTRAL_MODELS" "MISTRAL_API_KEY" "MISTRAL_API_KEYS"
526
- inject_provider_models_from_env "xai" "XAI_MODELS" "XAI_API_KEY" "XAI_API_KEYS"
527
- inject_provider_models_from_env "x-ai" "XAI_MODELS" "XAI_API_KEY" "XAI_API_KEYS"
528
- inject_provider_models_from_env "nvidia" "NVIDIA_MODELS" "NVIDIA_API_KEY" "NVIDIA_API_KEYS"
529
- inject_provider_models_from_env "cohere" "COHERE_MODELS" "COHERE_API_KEY" "COHERE_API_KEYS"
530
- inject_provider_models_from_env "together" "TOGETHER_MODELS" "TOGETHER_API_KEY" "TOGETHER_API_KEYS"
531
- inject_provider_models_from_env "cerebras" "CEREBRAS_MODELS" "CEREBRAS_API_KEY" "CEREBRAS_API_KEYS"
532
- inject_provider_models_from_env "huggingface" "HUGGINGFACE_MODELS" "HUGGINGFACE_HUB_TOKEN" "HUGGINGFACE_HUB_TOKENS"
533
- inject_provider_models_from_env "venice" "VENICE_MODELS" "VENICE_API_KEY" "VENICE_API_KEYS"
534
  inject_provider_models_from_env "synthetic" "SYNTHETIC_MODELS" "SYNTHETIC_API_KEY" "SYNTHETIC_API_KEYS"
535
- inject_provider_models_from_env "github-copilot" "GITHUB_COPILOT_MODELS" "COPILOT_GITHUB_TOKEN" "COPILOT_GITHUB_TOKENS"
536
 
537
  # Browser configuration (managed local Chromium in HF/Docker)
538
  BROWSER_EXECUTABLE_PATH=""
@@ -740,8 +813,8 @@ if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
740
 
741
  export OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1
742
  export OPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first
743
- # Force ipv4 for Telegram specifically as HF IPv6 often times out
744
- export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--dns-result-order=ipv4first"
745
 
746
  CONFIG_JSON=$(echo "$CONFIG_JSON" | jq --arg token "$CLEAN_TG_TOKEN" --arg proxy_url "$TELEGRAM_API_ROOT" '
747
  .channels.telegram.enabled = true
@@ -839,10 +912,27 @@ if [ -f "$EXISTING_CONFIG" ]; then
839
  --argjson whatsappConfigured "$WHATSAPP_ENABLED_CONFIGURED" \
840
  --argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
841
  --argjson telegramConfigured "$TELEGRAM_CONFIG_ENABLED" \
 
842
  '(.channels.whatsapp // {}) as $existingWhatsapp
 
843
  | .gateway.auth.token = $token
844
  | .agents.defaults.model = $model
845
  | .gateway.port = ($desired.gateway.port // .gateway.port)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
846
  | if $fileLogConfigured then .logging.level = $fileLevel else . end
847
  | if $consoleLogConfigured then .logging.consoleLevel = $consoleLevel else . end
848
  | if $consoleStyleConfigured then .logging.consoleStyle = $consoleStyle else . end
@@ -856,10 +946,33 @@ if [ -f "$EXISTING_CONFIG" ]; then
856
  else
857
  .
858
  end
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
859
  | .channels = ((.channels // {}) * ($desired.channels // {}))
860
  | .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique)
861
  | .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique)
862
  | .plugins.entries = ((.plugins.entries // {}) * ($desired.plugins.entries // {}))
 
 
 
 
 
 
 
863
  | if $whatsappEnabled then
864
  ($desired.channels.whatsapp // {"dmPolicy": "pairing"}) as $desiredWhatsapp
865
  | .plugins.entries.whatsapp.enabled = true
@@ -873,8 +986,13 @@ if [ -f "$EXISTING_CONFIG" ]; then
873
  .
874
  end
875
  | if $telegramConfigured then
876
- .channels.telegram = (($desired.channels.telegram // {}) * (.channels.telegram // {}))
877
- | .channels.telegram.botToken = $desired.channels.telegram.botToken
 
 
 
 
 
878
  else
879
  del(.channels.telegram)
880
  | .plugins.entries.telegram.enabled = false
@@ -1790,6 +1908,25 @@ start_guardian_once() {
1790
  echo "WhatsApp Guardian started (PID: $GUARDIAN_PID)"
1791
  }
1792
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1793
  # ── Start D-Bus session (once, before gateway loop) ──
1794
  if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
1795
  if command -v dbus-launch >/dev/null 2>&1; then
@@ -1844,9 +1981,14 @@ while true; do
1844
  GATEWAY_PID=$!
1845
 
1846
  # Poll for the gateway to start listening on ${GATEWAY_PORT}. OpenClaw can take 20-30s
1847
- # on cold start (plugin install + auto-restore). Bail out early if the
1848
- # pipeline died.
1849
- GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-90}"
 
 
 
 
 
1850
  ready=false
1851
  for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do
1852
  if (echo > /dev/tcp/127.0.0.1/${GATEWAY_PORT}) 2>/dev/null; then
 
124
  ACP_PLUGIN_MODE="${ACP_PLUGIN_MODE:-disabled}"
125
  # HF Spaces does not benefit from Bonjour discovery, and the retries add noise.
126
  export OPENCLAW_DISABLE_BONJOUR="${OPENCLAW_DISABLE_BONJOUR:-1}"
127
+ # HF Spaces IPv6 routing is unreliable and causes ECONNRESET on outbound
128
+ # WebSocket connections (WhatsApp, Telegram, etc.) which triggers gateway
129
+ # channel restarts and floods logs with "ws closed before connect" (1006)
130
+ # errors. Force IPv4 globally for ALL channels on this Space.
131
+ # Previously this was only set inside the Telegram block β€” meaning
132
+ # WhatsApp-only deployments never got this fix and suffered ECONNRESET drops.
133
+ export NODE_OPTIONS="${NODE_OPTIONS:+$NODE_OPTIONS }--dns-result-order=ipv4first"
134
+ export OPENCLAW_WHATSAPP_DISABLE_AUTO_SELECT_FAMILY="${OPENCLAW_WHATSAPP_DISABLE_AUTO_SELECT_FAMILY:-1}"
135
+ export OPENCLAW_WHATSAPP_DNS_RESULT_ORDER="${OPENCLAW_WHATSAPP_DNS_RESULT_ORDER:-ipv4first}"
136
  else
137
  OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-info}"
138
  OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
 
463
  # NVIDIA_MODELS=model1,model2
464
  # OPENAI_MODELS=gpt-4o-mini,gpt-4.1
465
  # This helps when provider auto-discovery does not populate models reliably.
466
+ # Default catalogs (used when *_MODELS env is not set but key IS configured).
467
+ # These let multi-key pool users see models without having to also set *_MODELS.
468
+ _DEFAULT_ANTHROPIC_MODELS="anthropic/claude-opus-4-6,anthropic/claude-sonnet-4-6,anthropic/claude-haiku-4-5"
469
+ _DEFAULT_OPENAI_MODELS="openai/gpt-5.5,openai/gpt-5.4-mini,openai/gpt-4o,openai/gpt-4o-mini"
470
+ _DEFAULT_GEMINI_MODELS="google/gemini-3.1-pro-preview,google/gemini-3.1-flash-preview,google/gemini-2.5-pro,google/gemini-2.5-flash,google/gemini-2.0-flash"
471
+ _DEFAULT_VERTEX_MODELS="google-vertex/gemini-2.5-pro,google-vertex/gemini-2.5-flash,google-vertex/gemini-2.0-flash"
472
+ _DEFAULT_DEEPSEEK_MODELS="deepseek/deepseek-v4-pro,deepseek/deepseek-v4-flash,deepseek/deepseek-chat,deepseek/deepseek-reasoner"
473
+ _DEFAULT_OPENROUTER_MODELS="openrouter/auto,openrouter/anthropic/claude-opus-4-6,openrouter/openai/gpt-4o,openrouter/google/gemini-2.5-pro"
474
+ _DEFAULT_GROQ_MODELS="groq/compound-beta,groq/moonshotai/kimi-k2-5,groq/deepseek-r1-distill-llama-70b"
475
+ _DEFAULT_MISTRAL_MODELS="mistral/mistral-large-latest,mistral/codestral-latest,mistral/mistral-small-latest"
476
+ _DEFAULT_XAI_MODELS="xai/grok-4.3,xai/grok-3,xai/grok-3-mini"
477
+ _DEFAULT_COHERE_MODELS="cohere/command-r-plus,cohere/command-r"
478
+ _DEFAULT_TOGETHER_MODELS="together/moonshotai/Kimi-K2.5,together/deepseek-ai/DeepSeek-V3.2,together/meta-llama/Llama-4-Maverick-17B-128E-Instruct-FP8"
479
+ _DEFAULT_CEREBRAS_MODELS="cerebras/zai-glm-4.7,cerebras/llama-4-scout-17b-16e-instruct"
480
+ _DEFAULT_NVIDIA_MODELS="nvidia/nemotron-3-super-120b-a12b,nvidia/moonshotai/kimi-k2.5"
481
+ _DEFAULT_KILOCODE_MODELS="kilocode/kilo/auto"
482
+ _DEFAULT_MOONSHOT_MODELS="moonshot/kimi-k2.6,moonshot/kimi-k2.5,moonshot/kimi-k2-thinking"
483
+ _DEFAULT_MINIMAX_MODELS="minimax/MiniMax-M2.7,minimax/MiniMax-M2.5"
484
+ _DEFAULT_ZAI_MODELS="zai/glm-5.1,zai/glm-4.7"
485
+ _DEFAULT_MODELSTUDIO_MODELS="modelstudio/qwen3-max,modelstudio/qwen3-coder,modelstudio/qwen3-32b"
486
+ _DEFAULT_VENICE_MODELS="venice/llama-3.3-70b"
487
+ _DEFAULT_OPENCODE_MODELS="opencode/claude-opus-4-6,opencode/claude-sonnet-4-6"
488
+ _DEFAULT_HUGGINGFACE_MODELS="huggingface/deepseek-ai/DeepSeek-R1,huggingface/meta-llama/Llama-4-Scout-17B-16E-Instruct"
489
+ _DEFAULT_GITHUB_COPILOT_MODELS="github-copilot/gpt-5,github-copilot/gpt-4.1,github-copilot/gpt-4o"
490
+
491
  INJECTED_MODELS_PROVIDERS='{}'
492
  inject_provider_models_from_env() {
493
  local provider="$1"
494
  local models_env="$2"
495
  local key_env_single="$3"
496
  local key_env_pool="$4"
497
+ local default_models_env="${5:-}" # Optional 5th arg: fallback default models var name
498
  local models_csv="${!models_env:-}"
499
  local single_key="${!key_env_single:-}"
500
  local pool_keys="${!key_env_pool:-}"
501
 
502
+ # Need at least one configured key
503
+ if [ -z "$single_key" ] && [ -z "$pool_keys" ]; then
504
+ return 0
505
+ fi
506
+
507
+ # If no explicit model list but a default var was provided, fall back to it
508
+ if [ -z "$models_csv" ] && [ -n "$default_models_env" ]; then
509
+ models_csv="${!default_models_env:-}"
510
+ fi
511
+
512
+ # Still nothing to inject
513
+ if [ -z "$models_csv" ]; then
514
  return 0
515
  fi
516
 
 
521
  | awk 'NF' \
522
  | jq -R . \
523
  | jq -s --arg provider "$provider" '
524
+ map(
525
+ if contains("/") then
526
+ # Fix cross-prefix: strip any foreign provider prefix, reapply correct one.
527
+ # e.g. "google/gemini-2.5-pro" injected into "google-vertex" becomes
528
+ # "google-vertex/gemini-2.5-pro"
529
+ if startswith($provider + "/") then .
530
+ else ($provider + "/" + (split("/") | last))
531
+ end
532
+ else
533
+ ($provider + "/" + .)
534
+ end
535
+ )
536
  | map({id: ., name: .})
537
  | unique_by(.id)')
538
 
 
548
  '.[$provider] = ((.[$provider] // {}) + {models: $models})' <<<"$INJECTED_MODELS_PROVIDERS")
549
  }
550
 
551
+ # ── Google Vertex AI credentials setup ──
552
+ # Vertex AI uses GCP project + location, NOT a simple Gemini API key.
553
+ # Set GOOGLE_CLOUD_PROJECT, GOOGLE_CLOUD_LOCATION, and optionally
554
+ # GOOGLE_APPLICATION_CREDENTIALS_JSON (base64-encoded service account JSON).
555
+ if [ -n "${GOOGLE_APPLICATION_CREDENTIALS_JSON:-}" ]; then
556
+ _VERTEX_CREDS_FILE="/tmp/gcp-service-account.json"
557
+ printf '%s' "$GOOGLE_APPLICATION_CREDENTIALS_JSON" | base64 -d > "$_VERTEX_CREDS_FILE" 2>/dev/null \
558
+ || printf '%s' "$GOOGLE_APPLICATION_CREDENTIALS_JSON" > "$_VERTEX_CREDS_FILE"
559
+ export GOOGLE_APPLICATION_CREDENTIALS="$_VERTEX_CREDS_FILE"
560
+ echo "Note: GOOGLE_APPLICATION_CREDENTIALS written from GOOGLE_APPLICATION_CREDENTIALS_JSON"
561
+ fi
562
+ [ -n "${GOOGLE_CLOUD_PROJECT:-}" ] && export GOOGLE_CLOUD_PROJECT
563
+ [ -n "${GOOGLE_CLOUD_LOCATION:-}" ] && export GOOGLE_CLOUD_LOCATION
564
+
565
  # Built-in provider model envs (optional)
566
+ inject_provider_models_from_env "anthropic" "ANTHROPIC_MODELS" "ANTHROPIC_API_KEY" "ANTHROPIC_API_KEYS" "_DEFAULT_ANTHROPIC_MODELS"
567
+ inject_provider_models_from_env "openai" "OPENAI_MODELS" "OPENAI_API_KEY" "OPENAI_API_KEYS" "_DEFAULT_OPENAI_MODELS"
568
+ inject_provider_models_from_env "openai-codex" "OPENAI_MODELS" "OPENAI_API_KEY" "OPENAI_API_KEYS" "_DEFAULT_OPENAI_MODELS"
569
+ inject_provider_models_from_env "google" "GEMINI_MODELS" "GEMINI_API_KEY" "GEMINI_API_KEYS" "_DEFAULT_GEMINI_MODELS"
570
+ # google-vertex: uses VERTEX_MODELS (with google-vertex/ prefix) separately from GEMINI_MODELS.
571
+ # The "key" check uses GOOGLE_CLOUD_PROJECT so it only injects when Vertex is actually configured.
572
+ # google-vertex: inject when GOOGLE_CLOUD_PROJECT is configured.
573
+ # Pool key uses a dummy var (_VERTEX_POOL_UNUSED) so that only GOOGLE_CLOUD_PROJECT
574
+ # gates injection β€” Vertex uses GCP project auth, not Gemini API key rotation.
575
+ inject_provider_models_from_env "google-vertex" "VERTEX_MODELS" "GOOGLE_CLOUD_PROJECT" "_VERTEX_POOL_UNUSED" "_DEFAULT_VERTEX_MODELS"
576
+ inject_provider_models_from_env "deepseek" "DEEPSEEK_MODELS" "DEEPSEEK_API_KEY" "DEEPSEEK_API_KEYS" "_DEFAULT_DEEPSEEK_MODELS"
577
+ inject_provider_models_from_env "openrouter" "OPENROUTER_MODELS" "OPENROUTER_API_KEY" "OPENROUTER_API_KEYS" "_DEFAULT_OPENROUTER_MODELS"
578
+ inject_provider_models_from_env "kilocode" "KILOCODE_MODELS" "KILOCODE_API_KEY" "KILOCODE_API_KEYS" "_DEFAULT_KILOCODE_MODELS"
579
+ inject_provider_models_from_env "opencode" "OPENCODE_MODELS" "OPENCODE_API_KEY" "OPENCODE_API_KEYS" "_DEFAULT_OPENCODE_MODELS"
580
+ inject_provider_models_from_env "opencode-go" "OPENCODE_MODELS" "OPENCODE_API_KEY" "OPENCODE_API_KEYS" "_DEFAULT_OPENCODE_MODELS"
581
+ inject_provider_models_from_env "zai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS" "_DEFAULT_ZAI_MODELS"
582
+ inject_provider_models_from_env "z-ai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS" "_DEFAULT_ZAI_MODELS"
583
+ inject_provider_models_from_env "z.ai" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS" "_DEFAULT_ZAI_MODELS"
584
+ inject_provider_models_from_env "zhipu" "ZAI_MODELS" "ZAI_API_KEY" "ZAI_API_KEYS" "_DEFAULT_ZAI_MODELS"
585
+ inject_provider_models_from_env "moonshot" "MOONSHOT_MODELS" "MOONSHOT_API_KEY" "MOONSHOT_API_KEYS" "_DEFAULT_MOONSHOT_MODELS"
586
  inject_provider_models_from_env "kimi-coding" "KIMI_MODELS" "KIMI_API_KEY" "KIMI_API_KEYS"
587
+ inject_provider_models_from_env "minimax" "MINIMAX_MODELS" "MINIMAX_API_KEY" "MINIMAX_API_KEYS" "_DEFAULT_MINIMAX_MODELS"
588
+ inject_provider_models_from_env "modelstudio" "MODELSTUDIO_MODELS" "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS" "_DEFAULT_MODELSTUDIO_MODELS"
589
+ inject_provider_models_from_env "qwen" "MODELSTUDIO_MODELS" "MODELSTUDIO_API_KEY" "MODELSTUDIO_API_KEYS" "_DEFAULT_MODELSTUDIO_MODELS"
590
  inject_provider_models_from_env "xiaomi" "XIAOMI_MODELS" "XIAOMI_API_KEY" "XIAOMI_API_KEYS"
591
  inject_provider_models_from_env "volcengine" "VOLCANO_ENGINE_MODELS" "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS"
592
  inject_provider_models_from_env "volcengine-plan" "VOLCANO_ENGINE_MODELS" "VOLCANO_ENGINE_API_KEY" "VOLCANO_ENGINE_API_KEYS"
593
  inject_provider_models_from_env "byteplus" "BYTEPLUS_MODELS" "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS"
594
  inject_provider_models_from_env "byteplus-plan" "BYTEPLUS_MODELS" "BYTEPLUS_API_KEY" "BYTEPLUS_API_KEYS"
595
  inject_provider_models_from_env "qianfan" "QIANFAN_MODELS" "QIANFAN_API_KEY" "QIANFAN_API_KEYS"
596
+ inject_provider_models_from_env "groq" "GROQ_MODELS" "GROQ_API_KEY" "GROQ_API_KEYS" "_DEFAULT_GROQ_MODELS"
597
+ inject_provider_models_from_env "mistral" "MISTRAL_MODELS" "MISTRAL_API_KEY" "MISTRAL_API_KEYS" "_DEFAULT_MISTRAL_MODELS"
598
+ inject_provider_models_from_env "mistralai" "MISTRAL_MODELS" "MISTRAL_API_KEY" "MISTRAL_API_KEYS" "_DEFAULT_MISTRAL_MODELS"
599
+ inject_provider_models_from_env "xai" "XAI_MODELS" "XAI_API_KEY" "XAI_API_KEYS" "_DEFAULT_XAI_MODELS"
600
+ inject_provider_models_from_env "x-ai" "XAI_MODELS" "XAI_API_KEY" "XAI_API_KEYS" "_DEFAULT_XAI_MODELS"
601
+ inject_provider_models_from_env "nvidia" "NVIDIA_MODELS" "NVIDIA_API_KEY" "NVIDIA_API_KEYS" "_DEFAULT_NVIDIA_MODELS"
602
+ inject_provider_models_from_env "cohere" "COHERE_MODELS" "COHERE_API_KEY" "COHERE_API_KEYS" "_DEFAULT_COHERE_MODELS"
603
+ inject_provider_models_from_env "together" "TOGETHER_MODELS" "TOGETHER_API_KEY" "TOGETHER_API_KEYS" "_DEFAULT_TOGETHER_MODELS"
604
+ inject_provider_models_from_env "cerebras" "CEREBRAS_MODELS" "CEREBRAS_API_KEY" "CEREBRAS_API_KEYS" "_DEFAULT_CEREBRAS_MODELS"
605
+ inject_provider_models_from_env "huggingface" "HUGGINGFACE_MODELS" "HUGGINGFACE_HUB_TOKEN" "HUGGINGFACE_HUB_TOKENS" "_DEFAULT_HUGGINGFACE_MODELS"
606
+ inject_provider_models_from_env "venice" "VENICE_MODELS" "VENICE_API_KEY" "VENICE_API_KEYS" "_DEFAULT_VENICE_MODELS"
607
  inject_provider_models_from_env "synthetic" "SYNTHETIC_MODELS" "SYNTHETIC_API_KEY" "SYNTHETIC_API_KEYS"
608
+ inject_provider_models_from_env "github-copilot" "GITHUB_COPILOT_MODELS" "COPILOT_GITHUB_TOKEN" "COPILOT_GITHUB_TOKENS" "_DEFAULT_GITHUB_COPILOT_MODELS"
609
 
610
  # Browser configuration (managed local Chromium in HF/Docker)
611
  BROWSER_EXECUTABLE_PATH=""
 
813
 
814
  export OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1
815
  export OPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first
816
+ # Note: --dns-result-order=ipv4first is now set globally for all HF Space
817
+ # channels in the SPACE_HOST block above; no need to set NODE_OPTIONS here.
818
 
819
  CONFIG_JSON=$(echo "$CONFIG_JSON" | jq --arg token "$CLEAN_TG_TOKEN" --arg proxy_url "$TELEGRAM_API_ROOT" '
820
  .channels.telegram.enabled = true
 
912
  --argjson whatsappConfigured "$WHATSAPP_ENABLED_CONFIGURED" \
913
  --argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
914
  --argjson telegramConfigured "$TELEGRAM_CONFIG_ENABLED" \
915
+ --argjson browserEnabled "$BROWSER_SHOULD_ENABLE" \
916
  '(.channels.whatsapp // {}) as $existingWhatsapp
917
+ | (.channels.telegram // {}) as $existingTelegram
918
  | .gateway.auth.token = $token
919
  | .agents.defaults.model = $model
920
  | .gateway.port = ($desired.gateway.port // .gateway.port)
921
+ | .gateway.controlUi.dangerouslyDisableDeviceAuth = true
922
+ | (if ($desired.gateway.controlUi.allowedOrigins // [] | length) > 0 then
923
+ .gateway.controlUi.allowedOrigins = (
924
+ ((.gateway.controlUi.allowedOrigins // []) + ($desired.gateway.controlUi.allowedOrigins // []))
925
+ | unique
926
+ )
927
+ else . end)
928
+ | (if ($desired.gateway.auth.mode // "") != "" then
929
+ .gateway.auth.mode = $desired.gateway.auth.mode
930
+ | .gateway.auth.password = ($desired.gateway.auth.password // "")
931
+ else . end)
932
+ | .gateway.trustedProxies = (
933
+ ((.gateway.trustedProxies // []) + ($desired.gateway.trustedProxies // []))
934
+ | unique
935
+ )
936
  | if $fileLogConfigured then .logging.level = $fileLevel else . end
937
  | if $consoleLogConfigured then .logging.consoleLevel = $consoleLevel else . end
938
  | if $consoleStyleConfigured then .logging.consoleStyle = $consoleStyle else . end
 
946
  else
947
  .
948
  end
949
+ | if (($desired.models.providers // {} | length) > 0) then
950
+ reduce ($desired.models.providers // {} | to_entries)[] as $pe (.;
951
+ # Propagate custom/new providers from desired config that are absent in existing.
952
+ # For known providers that already exist, only merge in baseUrl/apiKey/api when missing.
953
+ if .models.providers[$pe.key] == null then
954
+ .models.providers[$pe.key] = $pe.value
955
+ else
956
+ .models.providers[$pe.key] = (
957
+ {"baseUrl": $pe.value.baseUrl, "apiKey": $pe.value.apiKey, "api": $pe.value.api}
958
+ | with_entries(select(.value != null and .value != ""))
959
+ ) * (.models.providers[$pe.key] // {})
960
+ end
961
+ )
962
+ else
963
+ .
964
+ end
965
  | .channels = ((.channels // {}) * ($desired.channels // {}))
966
  | .plugins.allow = (((.plugins.allow // []) + ($desired.plugins.allow // [])) | unique)
967
  | .plugins.deny = (((.plugins.deny // []) + ($desired.plugins.deny // [])) | unique)
968
  | .plugins.entries = ((.plugins.entries // {}) * ($desired.plugins.entries // {}))
969
+ | del(.plugins.entries.acpx)
970
+ | (if $browserEnabled then
971
+ .browser = ($desired.browser // .browser)
972
+ else
973
+ .browser.enabled = false
974
+ | .plugins.entries.browser.enabled = false
975
+ end)
976
  | if $whatsappEnabled then
977
  ($desired.channels.whatsapp // {"dmPolicy": "pairing"}) as $desiredWhatsapp
978
  | .plugins.entries.whatsapp.enabled = true
 
986
  .
987
  end
988
  | if $telegramConfigured then
989
+ # Merge: existing * desired β†’ desired (env-driven) wins for runtime fields
990
+ # (apiRoot from CLOUDFLARE_PROXY_URL, commands.native, timeoutSeconds, retry).
991
+ # Then re-apply user-editable fields from saved $existingTelegram so UI
992
+ # customizations (dmPolicy, allowFrom) survive across reboots.
993
+ .channels.telegram = ($existingTelegram * ($desired.channels.telegram // {}))
994
+ | (if ($existingTelegram | has("dmPolicy")) then .channels.telegram.dmPolicy = $existingTelegram.dmPolicy else . end)
995
+ | (if ($existingTelegram | has("allowFrom")) then .channels.telegram.allowFrom = $existingTelegram.allowFrom else . end)
996
  else
997
  del(.channels.telegram)
998
  | .plugins.entries.telegram.enabled = false
 
1908
  echo "WhatsApp Guardian started (PID: $GUARDIAN_PID)"
1909
  }
1910
 
1911
+ # ── Clean up stale plugin-skills entries that are not generated symlinks ──
1912
+ # OpenClaw generates plugin-skills entries as symlinks. If a real directory
1913
+ # exists (e.g. from a previous failed install or manual placement), OpenClaw
1914
+ # logs "plugin skill entry is not a generated symlink" on every poll cycle.
1915
+ # Remove any non-symlink entries so they are regenerated cleanly on startup.
1916
+ PLUGIN_SKILLS_DIR="/home/node/.openclaw/plugin-skills"
1917
+ if [ -d "$PLUGIN_SKILLS_DIR" ]; then
1918
+ for _ps_entry in "$PLUGIN_SKILLS_DIR"/*/; do
1919
+ _ps_entry="${_ps_entry%/}"
1920
+ [ -e "$_ps_entry" ] || continue
1921
+ if [ ! -L "$_ps_entry" ]; then
1922
+ _ps_name="$(basename "$_ps_entry")"
1923
+ echo "Removing stale plugin-skills entry '$_ps_name' (not a generated symlink; will be regenerated by OpenClaw)..."
1924
+ rm -rf "$_ps_entry"
1925
+ fi
1926
+ done
1927
+ unset _ps_entry _ps_name
1928
+ fi
1929
+
1930
  # ── Start D-Bus session (once, before gateway loop) ──
1931
  if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
1932
  if command -v dbus-launch >/dev/null 2>&1; then
 
1981
  GATEWAY_PID=$!
1982
 
1983
  # Poll for the gateway to start listening on ${GATEWAY_PORT}. OpenClaw can take 20-30s
1984
+ # on cold start (plugin install + auto-restore). On HF Spaces the bootstrap-context
1985
+ # stage alone can exceed 300 s on a cold start, so default to 300 s there and
1986
+ # 90 s elsewhere. Bail out early if the pipeline died.
1987
+ if [ -n "${SPACE_HOST:-}" ]; then
1988
+ GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-300}"
1989
+ else
1990
+ GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-90}"
1991
+ fi
1992
  ready=false
1993
  for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do
1994
  if (echo > /dev/tcp/127.0.0.1/${GATEWAY_PORT}) 2>/dev/null; then