Anurag commited on
Commit
6c56d83
Β·
1 Parent(s): c808e75

Restore LLM_MODEL picker type and catalog binding

Browse files
Files changed (5) hide show
  1. env-builder.html +0 -15
  2. env-builder.js +61 -48
  3. health-server.js +19 -9
  4. multi-provider-key-rotator.cjs +7 -0
  5. start.sh +13 -12
env-builder.html CHANGED
@@ -901,21 +901,6 @@ body {
901
  <aside class="right-panel">
902
  <div class="panel-scroll">
903
 
904
- <!-- Summary -->
905
- <div class="pblock">
906
- <div class="pblock-head">
907
- <span class="pblock-title">🧭 Quick Guide</span>
908
- </div>
909
- <div class="pblock-body">
910
- <ol class="quick-guide">
911
- <li>Click <strong>⚑ Required</strong> to select must-have vars.</li>
912
- <li>Fill values in selected cards (search works by key/tag/group).</li>
913
- <li>Click <strong># Generate Bundle</strong> and copy Bundle/Env line.</li>
914
- <li>Paste into HF Space variables or use <strong>↓ Import & Apply</strong>.</li>
915
- </ol>
916
- </div>
917
- </div>
918
-
919
  <div class="pblock">
920
  <div class="pblock-head">
921
  <span class="pblock-title">πŸ“Š Summary</span>
 
901
  <aside class="right-panel">
902
  <div class="panel-scroll">
903
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
904
  <div class="pblock">
905
  <div class="pblock-head">
906
  <span class="pblock-title">πŸ“Š Summary</span>
env-builder.js CHANGED
@@ -54,25 +54,25 @@ const MODEL_CATALOGS = {
54
  "Groq": [
55
  "groq/compound",
56
  "groq/compound-mini",
57
- "llama-3.1-8b-instant",
58
- "llama-3.1-70b-versatile",
59
- "llama-3.3-70b-versatile",
60
  "meta-llama/llama-4-scout-17b-16e-instruct",
61
  "openai/gpt-oss-20b",
62
  "openai/gpt-oss-120b",
63
  "qwen/qwen3-32b",
64
- "mixtral-8x7b-32768"
65
  ],
66
  "Mistral": [
67
- "mistral-large-latest",
68
- "mistral-large-2",
69
- "mistral-medium-3.5",
70
- "mistral-small-latest",
71
- "mistral-small-3.2",
72
- "devstral-2",
73
- "ocr-3-premier",
74
- "voxtral-mini-transcribe-realtime",
75
- "codestral-latest"
76
  ],
77
  "Cohere": [
78
  "command-a",
@@ -280,25 +280,25 @@ const MODEL_CATALOGS = {
280
  "GROQ_MODELS": [
281
  "groq/compound",
282
  "groq/compound-mini",
283
- "llama-3.1-8b-instant",
284
- "llama-3.1-70b-versatile",
285
- "llama-3.3-70b-versatile",
286
  "openai/gpt-oss-20b",
287
  "openai/gpt-oss-120b",
288
  "meta-llama/llama-4-scout-17b-16e-instruct",
289
  "qwen/qwen3-32b",
290
- "mixtral-8x7b-32768"
291
  ],
292
  "MISTRAL_MODELS": [
293
- "mistral-large-latest",
294
- "mistral-large-2",
295
- "mistral-medium-3.5",
296
- "mistral-small-latest",
297
- "mistral-small-3.2",
298
- "devstral-2",
299
- "ocr-3-premier",
300
- "voxtral-mini-transcribe-realtime",
301
- "codestral-latest"
302
  ],
303
  "XAI_MODELS": [
304
  "grok-4.3",
@@ -1680,8 +1680,7 @@ const FIELDS = [
1680
  "icon": "πŸ”Œ",
1681
  "k": "CUSTOM_MODEL_ID",
1682
  "lbl": "Model ID",
1683
- "type": "model",
1684
- "options_key": "LLM_MODEL",
1685
  "ph": "custom model id",
1686
  "tag": "feature"
1687
  },
@@ -1732,7 +1731,7 @@ const FIELDS = [
1732
  "k": "CUSTOM_MAX_TOKENS",
1733
  "lbl": "Max output tokens",
1734
  "type": "number",
1735
- "ph": "500",
1736
  "tag": "advanced"
1737
  },
1738
  {
@@ -2061,6 +2060,7 @@ function defaultValueFor(field) {
2061
  }
2062
 
2063
  function valueControlHTML(field) {
 
2064
  const key = esc(field.k);
2065
  const placeholder = esc(field.ph || field.lbl || '');
2066
  const isSecret = !!field.secret;
@@ -2100,7 +2100,6 @@ function valueControlHTML(field) {
2100
  ${control}
2101
  </div>`;
2102
 
2103
- return control;
2104
  }
2105
 
2106
  function cardHTML(f, origIdx = 0) {
@@ -2418,23 +2417,31 @@ function bindFieldEvents() {
2418
 
2419
  function renderSections() {
2420
  const grouped = {};
2421
- FIELDS.forEach(f => { (grouped[f.g] ||= []).push(f); });
 
 
 
2422
 
2423
  const wrap = $('sections');
 
2424
  wrap.innerHTML = '';
2425
  Object.entries(grouped).forEach(([grp, items]) => {
2426
- const sec = document.createElement('div');
2427
- sec.className = 'sec';
2428
- sec.dataset.section = grp;
2429
- sec.innerHTML = `
2430
- <div class="sec-header">
2431
- <span class="sec-icon">${ICONS[grp] || 'πŸ“'}</span>
2432
- <span class="sec-title">${esc(grp)}</span>
2433
- <span class="sec-count">${items.length}</span>
2434
- <div class="sec-line"></div>
2435
- </div>
2436
- <div class="cards">${items.map((f, i) => cardHTML(f, i)).join('')}</div>`;
2437
- wrap.appendChild(sec);
 
 
 
 
2438
  });
2439
  bindFieldEvents();
2440
  }
@@ -2457,11 +2464,17 @@ function copyText(text) {
2457
  }
2458
 
2459
  // ── Init ──
2460
- renderSidebar();
2461
- renderSections();
2462
- addCustomRow();
2463
- filter();
2464
- refresh();
 
 
 
 
 
 
2465
 
2466
  // ── Events ──
2467
  $('search').oninput = filter;
 
54
  "Groq": [
55
  "groq/compound",
56
  "groq/compound-mini",
57
+ "groq/llama-3.1-8b-instant",
58
+ "groq/llama-3.1-70b-versatile",
59
+ "groq/llama-3.3-70b-versatile",
60
  "meta-llama/llama-4-scout-17b-16e-instruct",
61
  "openai/gpt-oss-20b",
62
  "openai/gpt-oss-120b",
63
  "qwen/qwen3-32b",
64
+ "groq/mixtral-8x7b-32768"
65
  ],
66
  "Mistral": [
67
+ "mistral/mistral-large-latest",
68
+ "mistral/mistral-large-2",
69
+ "mistral/mistral-medium-3.5",
70
+ "mistral/mistral-small-latest",
71
+ "mistral/mistral-small-3.2",
72
+ "mistral/devstral-2",
73
+ "mistral/ocr-3-premier",
74
+ "mistral/voxtral-mini-transcribe-realtime",
75
+ "mistral/codestral-latest"
76
  ],
77
  "Cohere": [
78
  "command-a",
 
280
  "GROQ_MODELS": [
281
  "groq/compound",
282
  "groq/compound-mini",
283
+ "groq/llama-3.1-8b-instant",
284
+ "groq/llama-3.1-70b-versatile",
285
+ "groq/llama-3.3-70b-versatile",
286
  "openai/gpt-oss-20b",
287
  "openai/gpt-oss-120b",
288
  "meta-llama/llama-4-scout-17b-16e-instruct",
289
  "qwen/qwen3-32b",
290
+ "groq/mixtral-8x7b-32768"
291
  ],
292
  "MISTRAL_MODELS": [
293
+ "mistral/mistral-large-latest",
294
+ "mistral/mistral-large-2",
295
+ "mistral/mistral-medium-3.5",
296
+ "mistral/mistral-small-latest",
297
+ "mistral/mistral-small-3.2",
298
+ "mistral/devstral-2",
299
+ "mistral/ocr-3-premier",
300
+ "mistral/voxtral-mini-transcribe-realtime",
301
+ "mistral/codestral-latest"
302
  ],
303
  "XAI_MODELS": [
304
  "grok-4.3",
 
1680
  "icon": "πŸ”Œ",
1681
  "k": "CUSTOM_MODEL_ID",
1682
  "lbl": "Model ID",
1683
+ "type": "text",
 
1684
  "ph": "custom model id",
1685
  "tag": "feature"
1686
  },
 
1731
  "k": "CUSTOM_MAX_TOKENS",
1732
  "lbl": "Max output tokens",
1733
  "type": "number",
1734
+ "ph": "8192",
1735
  "tag": "advanced"
1736
  },
1737
  {
 
2060
  }
2061
 
2062
  function valueControlHTML(field) {
2063
+ if (!field || !field.k) return '<span style="color:red">Invalid field</span>';
2064
  const key = esc(field.k);
2065
  const placeholder = esc(field.ph || field.lbl || '');
2066
  const isSecret = !!field.secret;
 
2100
  ${control}
2101
  </div>`;
2102
 
 
2103
  }
2104
 
2105
  function cardHTML(f, origIdx = 0) {
 
2417
 
2418
  function renderSections() {
2419
  const grouped = {};
2420
+ FIELDS.forEach(f => {
2421
+ if (!f || !f.g || !f.k) return;
2422
+ (grouped[f.g] ||= []).push(f);
2423
+ });
2424
 
2425
  const wrap = $('sections');
2426
+ if (!wrap) return;
2427
  wrap.innerHTML = '';
2428
  Object.entries(grouped).forEach(([grp, items]) => {
2429
+ try {
2430
+ const sec = document.createElement('div');
2431
+ sec.className = 'sec';
2432
+ sec.dataset.section = grp;
2433
+ sec.innerHTML = `
2434
+ <div class="sec-header">
2435
+ <span class="sec-icon">${ICONS[grp] || 'πŸ“'}</span>
2436
+ <span class="sec-title">${esc(grp)}</span>
2437
+ <span class="sec-count">${items.length}</span>
2438
+ <div class="sec-line"></div>
2439
+ </div>
2440
+ <div class="cards">${items.map((f, i) => { try { return cardHTML(f, i); } catch(e) { console.error('cardHTML error for field', f.k, e); return ''; } }).join('')}</div>`;
2441
+ wrap.appendChild(sec);
2442
+ } catch(e) {
2443
+ console.error('renderSections error for group', grp, e);
2444
+ }
2445
  });
2446
  bindFieldEvents();
2447
  }
 
2464
  }
2465
 
2466
  // ── Init ──
2467
+ try {
2468
+ renderSidebar();
2469
+ renderSections();
2470
+ addCustomRow();
2471
+ filter();
2472
+ refresh();
2473
+ } catch(e) {
2474
+ console.error('HuggingClaw ENV Builder init error:', e);
2475
+ const wrap = document.getElementById('sections');
2476
+ if (wrap) wrap.innerHTML = '<div style="color:red;padding:20px">ENV Builder failed to load. Open browser console for details. Error: ' + e.message + '</div>';
2477
+ }
2478
 
2479
  // ── Events ──
2480
  $('search').oninput = filter;
health-server.js CHANGED
@@ -22,10 +22,15 @@ const JUPYTER_HOST = "127.0.0.1";
22
  const JUPYTER_BASE = normalizeBase(process.env.JUPYTER_BASE, "/terminal");
23
  const GATEWAY_TOKEN = (process.env.GATEWAY_TOKEN || "").trim();
24
  const DEV_MODE_ENABLED = isTrue(process.env.DEV_MODE);
25
- // Default true. Only false when DEV_MODE=false or HUGGINGCLAW_JUPYTER_ENABLED=false is explicitly set.
 
 
26
  const JUPYTER_ENABLED =
27
- !/^(false|0|no|off)$/i.test(String(process.env.DEV_MODE || "").trim()) &&
28
- !/^(false|0|no|off)$/i.test(String(process.env.HUGGINGCLAW_JUPYTER_ENABLED || "").trim());
 
 
 
29
  const startTime = Date.now();
30
  const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
31
  const LLM_PROVIDER = LLM_MODEL.includes("/") ? LLM_MODEL.split("/")[0] : "";
@@ -100,7 +105,7 @@ async function detectSpacePrivacy() {
100
  return;
101
  }
102
 
103
- const token = (process.env.HF_TOKEN || "").trim();
104
  const reqOptions = {
105
  hostname: "huggingface.co",
106
  path: `/api/spaces/${SPACE_ID}`,
@@ -248,9 +253,14 @@ function parseCookies(req) {
248
 
249
  // Constant-time comparison β€” prevent timing attacks on token check
250
  function safeEqual(a, b) {
251
- if (typeof a !== "string" || typeof b !== "string" || a.length !== b.length) return false;
252
- let d = 0;
253
- for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
 
 
 
 
 
254
  return d === 0;
255
  }
256
 
@@ -664,7 +674,7 @@ const server = http.createServer(async (req, res) => {
664
  const body = await readBody(req);
665
  const token = decodeURIComponent((body.match(/(?:^|&)token=([^&]*)/) || [])[1] || "").replace(/\+/g, " ");
666
  if (safeEqual(token, GATEWAY_TOKEN)) {
667
- const cookie = `hc_env_auth=${encodeURIComponent(GATEWAY_TOKEN)}; Path=/; HttpOnly; SameSite=Strict; Max-Age=86400`;
668
  res.writeHead(302, { Location: "/env-builder", "Set-Cookie": cookie, "Cache-Control": "no-store" });
669
  return res.end();
670
  }
@@ -676,7 +686,7 @@ const server = http.createServer(async (req, res) => {
676
  }
677
 
678
  if (pathname === "/env-builder/logout") {
679
- res.writeHead(302, { Location: "/env-builder", "Set-Cookie": "hc_env_auth=; Path=/; HttpOnly; Max-Age=0", "Cache-Control": "no-store" });
680
  return res.end();
681
  }
682
 
 
22
  const JUPYTER_BASE = normalizeBase(process.env.JUPYTER_BASE, "/terminal");
23
  const GATEWAY_TOKEN = (process.env.GATEWAY_TOKEN || "").trim();
24
  const DEV_MODE_ENABLED = isTrue(process.env.DEV_MODE);
25
+ // Explicit HUGGINGCLAW_JUPYTER_ENABLED=true enables Jupyter.
26
+ // Otherwise DEV_MODE=true enables it unless HUGGINGCLAW_JUPYTER_ENABLED is explicitly false.
27
+ // HUGGINGCLAW_JUPYTER_ENABLED=true is the explicit user override and always wins.
28
  const JUPYTER_ENABLED =
29
+ /^(true|1|yes|on)$/i.test(String(process.env.HUGGINGCLAW_JUPYTER_ENABLED || "").trim()) ||
30
+ (
31
+ isTrue(process.env.DEV_MODE) &&
32
+ !/^(false|0|no|off)$/i.test(String(process.env.HUGGINGCLAW_JUPYTER_ENABLED || "").trim())
33
+ );
34
  const startTime = Date.now();
35
  const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
36
  const LLM_PROVIDER = LLM_MODEL.includes("/") ? LLM_MODEL.split("/")[0] : "";
 
105
  return;
106
  }
107
 
108
+ const token = (process.env.HF_TOKEN || process.env.HUGGINGFACE_HUB_TOKEN || "").trim();
109
  const reqOptions = {
110
  hostname: "huggingface.co",
111
  path: `/api/spaces/${SPACE_ID}`,
 
253
 
254
  // Constant-time comparison β€” prevent timing attacks on token check
255
  function safeEqual(a, b) {
256
+ if (typeof a !== "string" || typeof b !== "string") return false;
257
+ // Pad both to the same length so the loop always takes constant time,
258
+ // preventing token length from being leaked via early-return timing.
259
+ const len = Math.max(a.length, b.length, 1);
260
+ const pa = a.padEnd(len, "\0");
261
+ const pb = b.padEnd(len, "\0");
262
+ let d = a.length === b.length ? 0 : 1; // length mismatch β†’ always fail
263
+ for (let i = 0; i < len; i++) d |= pa.charCodeAt(i) ^ pb.charCodeAt(i);
264
  return d === 0;
265
  }
266
 
 
674
  const body = await readBody(req);
675
  const token = decodeURIComponent((body.match(/(?:^|&)token=([^&]*)/) || [])[1] || "").replace(/\+/g, " ");
676
  if (safeEqual(token, GATEWAY_TOKEN)) {
677
+ const cookie = `hc_env_auth=${encodeURIComponent(GATEWAY_TOKEN)}; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=86400`;
678
  res.writeHead(302, { Location: "/env-builder", "Set-Cookie": cookie, "Cache-Control": "no-store" });
679
  return res.end();
680
  }
 
686
  }
687
 
688
  if (pathname === "/env-builder/logout") {
689
+ res.writeHead(302, { Location: "/env-builder", "Set-Cookie": "hc_env_auth=; Path=/; HttpOnly; SameSite=None; Secure; Max-Age=0", "Cache-Control": "no-store" });
690
  return res.end();
691
  }
692
 
multi-provider-key-rotator.cjs CHANGED
@@ -86,6 +86,13 @@ const PROVIDERS = [
86
  envPlural: 'ZAI_API_KEYS',
87
  envSingular:'ZAI_API_KEY',
88
  },
 
 
 
 
 
 
 
89
  {
90
  name: 'moonshot',
91
  hostname: /(?:^|\.)api\.moonshot\.cn$/i,
 
86
  envPlural: 'ZAI_API_KEYS',
87
  envSingular:'ZAI_API_KEY',
88
  },
89
+ {
90
+ name: 'kimi-coding',
91
+ // kimi-coding routes through api.moonshot.cn; dedicated entry so KIMI_API_KEYS pool is used
92
+ hostname: /(?:^|\.)api\.moonshot\.cn$/i,
93
+ envPlural: 'KIMI_API_KEYS',
94
+ envSingular:'KIMI_API_KEY',
95
+ },
96
  {
97
  name: 'moonshot',
98
  hostname: /(?:^|\.)api\.moonshot\.cn$/i,
start.sh CHANGED
@@ -203,7 +203,7 @@ case "$LLM_PROVIDER" in
203
  byteplus|byteplus-plan) export BYTEPLUS_API_KEY="$LLM_API_KEY" ;;
204
  qianfan) export QIANFAN_API_KEY="$LLM_API_KEY" ;;
205
  # ── Western Providers ──
206
- mistral|mistralai) export MISTRAL_API_KEY="$LLM_API_KEY" ;;
207
  xai|x-ai) export XAI_API_KEY="$LLM_API_KEY" ;;
208
  nvidia) export NVIDIA_API_KEY="$LLM_API_KEY" ;;
209
  cohere) export COHERE_API_KEY="$LLM_API_KEY" ;;
@@ -214,8 +214,18 @@ case "$LLM_PROVIDER" in
214
  venice) export VENICE_API_KEY="$LLM_API_KEY" ;;
215
  synthetic) export SYNTHETIC_API_KEY="$LLM_API_KEY" ;;
216
  github-copilot) export COPILOT_GITHUB_TOKEN="$LLM_API_KEY" ;;
 
 
 
 
 
 
 
 
 
217
  # ── Fallback: Anthropic (default) ──
218
  *)
 
219
  export ANTHROPIC_API_KEY="$LLM_API_KEY"
220
  ;;
221
  esac
@@ -553,6 +563,7 @@ ensure_chromium_for_browser_plugin() {
553
  echo "ERROR: Browser plugin is enabled, but Chromium install failed. Disable browser plugin or rebuild image with Chromium preinstalled." >&2
554
  return 1
555
  }
 
556
  ensure_chromium_for_browser_plugin || HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
557
 
558
  # On Debian/Ubuntu, /usr/bin/chromium is often a shell wrapper while the real
@@ -1374,7 +1385,7 @@ fi
1374
  # Runs user-provided boot commands one by one so failures are visible in logs.
1375
  # By default failures are logged and boot continues; set
1376
  # HUGGINGCLAW_STARTUP_STRICT=true to fail the Space startup on any error.
1377
- HC_STARTUP_FAILURES=0
1378
  HC_STARTUP_STRICT_NORMALIZED=$(printf '%s' "${HUGGINGCLAW_STARTUP_STRICT:-false}" | tr '[:upper:]' '[:lower:]')
1379
  hc_run_startup_command() {
1380
  local source_label="$1"
@@ -1761,16 +1772,6 @@ while true; do
1761
  # 11. Start WhatsApp Guardian after the gateway is accepting connections
1762
  start_guardian_once
1763
 
1764
- # ── Silence D-Bus errors for headless Chromium ──
1765
- if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
1766
- if command -v dbus-launch >/dev/null 2>&1; then
1767
- eval "$(dbus-launch --sh-syntax 2>/dev/null)" || true
1768
- export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-disabled:}"
1769
- else
1770
- export DBUS_SESSION_BUS_ADDRESS="disabled:"
1771
- fi
1772
- fi
1773
-
1774
  # 11.5 Warm up the managed browser so first browser actions have a live tab
1775
  warmup_browser
1776
 
 
203
  byteplus|byteplus-plan) export BYTEPLUS_API_KEY="$LLM_API_KEY" ;;
204
  qianfan) export QIANFAN_API_KEY="$LLM_API_KEY" ;;
205
  # ── Western Providers ──
206
+ mistral) export MISTRAL_API_KEY="$LLM_API_KEY" ;;
207
  xai|x-ai) export XAI_API_KEY="$LLM_API_KEY" ;;
208
  nvidia) export NVIDIA_API_KEY="$LLM_API_KEY" ;;
209
  cohere) export COHERE_API_KEY="$LLM_API_KEY" ;;
 
214
  venice) export VENICE_API_KEY="$LLM_API_KEY" ;;
215
  synthetic) export SYNTHETIC_API_KEY="$LLM_API_KEY" ;;
216
  github-copilot) export COPILOT_GITHUB_TOKEN="$LLM_API_KEY" ;;
217
+ llama-3.*|llama-4.*|mixtral-*|gemma-*)
218
+ export GROQ_API_KEY="$LLM_API_KEY"
219
+ echo "Note: bare Groq model '$LLM_MODEL' detected; mapped LLM_API_KEY β†’ GROQ_API_KEY. Use 'groq/${LLM_MODEL}' prefix to be explicit." ;;
220
+ mistral-*|codestral-*|devstral-*|voxtral-*)
221
+ export MISTRAL_API_KEY="$LLM_API_KEY"
222
+ echo "Note: bare Mistral model '$LLM_MODEL' detected; mapped LLM_API_KEY β†’ MISTRAL_API_KEY. Use 'mistral/${LLM_MODEL}' prefix to be explicit." ;;
223
+ moonshotai|meta-llama|deepseek-ai|MiniMaxAI|minimax-ai|Qwen|zai-org|mistralai|google)
224
+ echo "Warning: LLM_MODEL='$LLM_MODEL' uses sub-provider prefix '$LLM_PROVIDER'. This is a router-namespaced model (Together/OpenRouter). Mapping LLM_API_KEY β†’ TOGETHER_API_KEY. If using OpenRouter, also set OPENROUTER_API_KEY as a separate secret."
225
+ export TOGETHER_API_KEY="${TOGETHER_API_KEY:-$LLM_API_KEY}" ;;
226
  # ── Fallback: Anthropic (default) ──
227
  *)
228
+ echo "Warning: Unknown provider prefix '$LLM_PROVIDER' in LLM_MODEL='$LLM_MODEL'. Defaulting to ANTHROPIC_API_KEY. If using a router-namespaced model (e.g. moonshotai/Kimi-K2.5), set TOGETHER_API_KEY or OPENROUTER_API_KEY as a separate secret."
229
  export ANTHROPIC_API_KEY="$LLM_API_KEY"
230
  ;;
231
  esac
 
563
  echo "ERROR: Browser plugin is enabled, but Chromium install failed. Disable browser plugin or rebuild image with Chromium preinstalled." >&2
564
  return 1
565
  }
566
+ HC_STARTUP_FAILURES=0
567
  ensure_chromium_for_browser_plugin || HC_STARTUP_FAILURES=$((HC_STARTUP_FAILURES + 1))
568
 
569
  # On Debian/Ubuntu, /usr/bin/chromium is often a shell wrapper while the real
 
1385
  # Runs user-provided boot commands one by one so failures are visible in logs.
1386
  # By default failures are logged and boot continues; set
1387
  # HUGGINGCLAW_STARTUP_STRICT=true to fail the Space startup on any error.
1388
+ # HC_STARTUP_FAILURES initialized earlier (before Chromium ensure check)
1389
  HC_STARTUP_STRICT_NORMALIZED=$(printf '%s' "${HUGGINGCLAW_STARTUP_STRICT:-false}" | tr '[:upper:]' '[:lower:]')
1390
  hc_run_startup_command() {
1391
  local source_label="$1"
 
1772
  # 11. Start WhatsApp Guardian after the gateway is accepting connections
1773
  start_guardian_once
1774
 
 
 
 
 
 
 
 
 
 
 
1775
  # 11.5 Warm up the managed browser so first browser actions have a live tab
1776
  warmup_browser
1777