Anurag commited on
Commit
73b4eda
Β·
1 Parent(s): d5203bf

Use GATEWAY_PORT in guardian and sync restored config port

Browse files
.env.example CHANGED
@@ -248,8 +248,8 @@ LLM_API_KEY_FALLBACK_ENABLED=true
248
  GATEWAY_TOKEN=your_gateway_token_here
249
 
250
  # [OPTIONAL] JupyterLab terminal token for /terminal/
251
- # Defaults to "huggingface" if unset. Set a strong token for private deployments.
252
- JUPYTER_TOKEN=huggingface
253
 
254
  # (Optional) Password auth β€” simpler alternative to token for casual users
255
  # If set, users can log in with this password instead of the token
@@ -288,14 +288,7 @@ HF_TOKEN=hf_your_token_here
288
  # Default: huggingclaw-backup
289
  BACKUP_DATASET_NAME=huggingclaw-backup
290
 
291
- # Git commit identity for workspace syncs
292
- WORKSPACE_GIT_USER=openclaw@example.com
293
- WORKSPACE_GIT_NAME=OpenClaw Bot
294
-
295
  # ── OPTIONAL: Background Services ──
296
- # Keep-alive ping interval (seconds). Default: 300. Set 0 to disable.
297
- KEEP_ALIVE_INTERVAL=300
298
-
299
  # Workspace auto-sync interval (seconds). Default: 180.
300
  SYNC_INTERVAL=180
301
 
 
248
  GATEWAY_TOKEN=your_gateway_token_here
249
 
250
  # [OPTIONAL] JupyterLab terminal token for /terminal/
251
+ # Set a strong token for private deployments. Must NOT be "huggingface".
252
+ JUPYTER_TOKEN=run: openssl rand -hex 32
253
 
254
  # (Optional) Password auth β€” simpler alternative to token for casual users
255
  # If set, users can log in with this password instead of the token
 
288
  # Default: huggingclaw-backup
289
  BACKUP_DATASET_NAME=huggingclaw-backup
290
 
 
 
 
 
291
  # ── OPTIONAL: Background Services ──
 
 
 
292
  # Workspace auto-sync interval (seconds). Default: 180.
293
  SYNC_INTERVAL=180
294
 
Dockerfile CHANGED
@@ -24,6 +24,8 @@ RUN apt-get update && apt-get install -y \
24
  ca-certificates \
25
  jq \
26
  curl \
 
 
27
  python3 \
28
  python3-pip \
29
  chromium \
 
24
  ca-certificates \
25
  jq \
26
  curl \
27
+ dbus \
28
+ dbus-x11 \
29
  python3 \
30
  python3-pip \
31
  chromium \
README.md CHANGED
@@ -104,7 +104,7 @@ Navigate to your new Space's **Settings**, scroll down to the **Variables and se
104
  > [!TIP]
105
  > HuggingClaw is completely flexible! You only need these three secrets to get started. You can set other secrets later.
106
 
107
- Optional: set `DEV_MODE=true` (Variable) to enable JupyterLab support and install Jupyter dependencies at build time. You can also set `JUPYTER_TOKEN` as a Secret to replace the default terminal token (`huggingface`). If you want to pin a specific OpenClaw release instead of `latest`, add `OPENCLAW_VERSION` under **Variables** in your Space settings. For Docker Spaces, HF passes Variables as build args during image build, so these should be Variables, not Secrets (except tokens).
108
 
109
  ### Step 3: Deploy & Run
110
 
@@ -366,12 +366,12 @@ The merged Space includes the Hugging Face JupyterLab template behavior inside t
366
  | :--- | :--- | :--- | :--- |
367
  | `/` | HuggingClaw dashboard | `7861` | Public HF Spaces entrypoint |
368
  | `/app/` | OpenClaw Control UI | `7860` | Mounted behind the local reverse proxy |
369
- | `/terminal/` | JupyterLab terminal (DEV_MODE only) | `8888` | Available only when `DEV_MODE=true`; token login uses `JUPYTER_TOKEN` (default `huggingface`) |
370
 
371
  When enabled, the terminal notebook root is `/home/node`, so you can inspect HuggingClaw files, logs, workspace state, and runtime scripts from the browser.
372
 
373
  > [!IMPORTANT]
374
- > For real deployments, set a strong `JUPYTER_TOKEN` secret. The `huggingface` default exists only to match the duplicateable Hugging Face JupyterLab template.
375
 
376
  ## πŸ” Merge Comparison
377
 
 
104
  > [!TIP]
105
  > HuggingClaw is completely flexible! You only need these three secrets to get started. You can set other secrets later.
106
 
107
+ Optional: set `DEV_MODE=true` (Variable) to enable JupyterLab support and install Jupyter dependencies at build time. You can also set `JUPYTER_TOKEN` as a Secret to set a strong terminal token (must not be `huggingface`). If you want to pin a specific OpenClaw release instead of `latest`, add `OPENCLAW_VERSION` under **Variables** in your Space settings. For Docker Spaces, HF passes Variables as build args during image build, so these should be Variables, not Secrets (except tokens).
108
 
109
  ### Step 3: Deploy & Run
110
 
 
366
  | :--- | :--- | :--- | :--- |
367
  | `/` | HuggingClaw dashboard | `7861` | Public HF Spaces entrypoint |
368
  | `/app/` | OpenClaw Control UI | `7860` | Mounted behind the local reverse proxy |
369
+ | `/terminal/` | JupyterLab terminal (DEV_MODE only) | `8888` | Available only when `DEV_MODE=true`; token login uses `JUPYTER_TOKEN` (set a strong value) |
370
 
371
  When enabled, the terminal notebook root is `/home/node`, so you can inspect HuggingClaw files, logs, workspace state, and runtime scripts from the browser.
372
 
373
  > [!IMPORTANT]
374
+ > For real deployments, set a strong `JUPYTER_TOKEN` secret. Do not use `huggingface`; generate a strong token with `openssl rand -hex 32`.
375
 
376
  ## πŸ” Merge Comparison
377
 
env-builder.html CHANGED
@@ -908,7 +908,8 @@ body {
908
  <span class="pblock-title">πŸ“¦ Bundle Output</span>
909
  </div>
910
  <div class="pblock-body">
911
- <textarea id="bundleOut" placeholder="Your encoded bundle will appear here…" readonly spellcheck="false"></textarea>
 
912
  <input type="text" id="envLineOut" placeholder="HUGGINGCLAW_ENV_BUNDLE=…" readonly spellcheck="false">
913
  <div class="row-btns">
914
  <button id="copyBundle" class="btn btn-amber">⎘ Bundle</button>
 
908
  <span class="pblock-title">πŸ“¦ Bundle Output</span>
909
  </div>
910
  <div class="pblock-body">
911
+ <button id="generateBundle" class="btn btn-amber" style="width:100%;font-size:12.5px;"># Generate Bundle</button>
912
+ <textarea id="bundleOut" placeholder="Click # Generate to build your bundle…" readonly spellcheck="false"></textarea>
913
  <input type="text" id="envLineOut" placeholder="HUGGINGCLAW_ENV_BUNDLE=…" readonly spellcheck="false">
914
  <div class="row-btns">
915
  <button id="copyBundle" class="btn btn-amber">⎘ Bundle</button>
env-builder.js CHANGED
@@ -458,10 +458,10 @@ const FIELDS = [
458
  "g": "Core",
459
  "icon": "⚑",
460
  "k": "OPENCLAW_VERSION",
461
- "lbl": "Pin OpenClaw version",
462
  "type": "text",
463
  "ph": "latest",
464
- "tag": "optional"
465
  },
466
  {
467
  "g": "Plugins",
@@ -482,6 +482,15 @@ const FIELDS = [
482
  "common": 1,
483
  "tag": "build"
484
  },
 
 
 
 
 
 
 
 
 
485
  {
486
  "g": "Startup",
487
  "icon": "⚑",
@@ -842,22 +851,12 @@ const FIELDS = [
842
  "g": "Core",
843
  "icon": "⚑",
844
  "k": "JUPYTER_TOKEN",
845
- "lbl": "Jupyter access token",
846
  "type": "password",
847
- "ph": "huggingface",
848
  "common": 1,
849
  "tag": "credential"
850
  },
851
- {
852
- "g": "Core",
853
- "icon": "⚑",
854
- "k": "KEEP_ALIVE_INTERVAL",
855
- "lbl": "Keep-alive ping interval (seconds)",
856
- "type": "number",
857
- "ph": "300",
858
- "common": 1,
859
- "tag": "advanced"
860
- },
861
  {
862
  "g": "Core",
863
  "icon": "⚑",
@@ -966,24 +965,6 @@ const FIELDS = [
966
  "ph": "/home/node",
967
  "tag": "advanced"
968
  },
969
- {
970
- "g": "Backup",
971
- "icon": "πŸ’Ύ",
972
- "k": "WORKSPACE_GIT_USER",
973
- "lbl": "Workspace git author email",
974
- "type": "text",
975
- "ph": "openclaw@example.com",
976
- "tag": "optional"
977
- },
978
- {
979
- "g": "Backup",
980
- "icon": "πŸ’Ύ",
981
- "k": "WORKSPACE_GIT_NAME",
982
- "lbl": "Workspace git author name",
983
- "type": "text",
984
- "ph": "OpenClaw Bot",
985
- "tag": "optional"
986
- },
987
  {
988
  "g": "Provider Keys",
989
  "icon": "πŸ”‘",
@@ -2123,7 +2104,7 @@ function valueControlHTML(field) {
2123
  return control;
2124
  }
2125
 
2126
- function cardHTML(f) {
2127
  const TAG_META = {
2128
  critical: { cls: 'badge-critical', lbl: 'critical' },
2129
  credential: { cls: 'badge-credential', lbl: 'credential' },
@@ -2135,7 +2116,7 @@ function cardHTML(f) {
2135
  const tm = TAG_META[f.tag] || TAG_META.optional;
2136
  const badge = `<span class="badge ${tm.cls}">${tm.lbl}</span>`;
2137
 
2138
- return `<div class="env-card" data-row data-group="${esc(f.g)}" data-search="${esc((f.g + ' ' + f.k + ' ' + (f.lbl || '') + ' ' + (f.tag || '')).toLowerCase())}">
2139
  <div class="card-top">
2140
  <input type="checkbox" class="card-check" data-check="${esc(f.k)}" ${f.common ? 'data-common="1"' : ''}>
2141
  <div class="card-info">
@@ -2213,14 +2194,17 @@ function collect() {
2213
  return obj;
2214
  }
2215
 
2216
- function refresh() {
2217
  const obj = collect();
2218
  const keys = Object.keys(obj).sort();
2219
  const bundle = keys.length ? encodeBundle(Object.fromEntries(keys.map(k => [k, obj[k]]))) : '';
2220
-
2221
  $('bundleOut').value = bundle;
2222
  $('envLineOut').value = bundle ? `HUGGINGCLAW_ENV_BUNDLE=${bundle}` : '';
 
2223
 
 
 
 
2224
  const s = $('summary');
2225
  if (keys.length) {
2226
  s.innerHTML = `<strong>${keys.length}</strong> variable${keys.length > 1 ? 's' : ''} selected<div class="sum-keys">${keys.map(k => `<span class="sum-key">${esc(k)}</span>`).join('')}</div>`;
@@ -2309,7 +2293,7 @@ function applyObj(obj, replace = false) {
2309
  addCustomRow(key, val, true);
2310
  }
2311
  }
2312
- markSelected(); filter(); refresh();
2313
  }
2314
 
2315
  function autoCheck(key) {
@@ -2398,13 +2382,34 @@ function toggleField(key) {
2398
  const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
2399
  if (chk) {
2400
  chk.checked = on;
 
2401
  markSelected();
2402
  }
2403
  refresh();
2404
  }
2405
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2406
  function bindFieldEvents() {
2407
- document.querySelectorAll('[data-check]').forEach(el => el.addEventListener('change', () => { markSelected(); refresh(); }));
2408
  document.querySelectorAll('[data-key]').forEach(el => el.addEventListener('input', refresh));
2409
  document.querySelectorAll('[data-toggle]').forEach(btn => btn.addEventListener('click', () => toggleField(btn.dataset.toggle)));
2410
  document.querySelectorAll('[data-pick-for]').forEach(sel => sel.addEventListener('change', () => handlePickerChange(sel)));
@@ -2429,7 +2434,7 @@ function renderSections() {
2429
  <span class="sec-count">${items.length}</span>
2430
  <div class="sec-line"></div>
2431
  </div>
2432
- <div class="cards">${items.map(cardHTML).join('')}</div>`;
2433
  wrap.appendChild(sec);
2434
  });
2435
  bindFieldEvents();
@@ -2463,56 +2468,40 @@ refresh();
2463
  $('search').oninput = filter;
2464
  $('selectCommon').onclick = () => {
2465
  document.querySelectorAll('[data-common="1"]').forEach(c => c.checked = true);
 
2466
  markSelected();
2467
  refresh();
2468
  };
2469
  $('selectVisible').onclick = () => {
2470
  document.querySelectorAll('.sec:not(.sec-hidden) [data-row]:not(.hidden) [data-check]').forEach(c => c.checked = true);
 
2471
  markSelected();
2472
  refresh();
2473
  };
2474
  $('clearAll').onclick = () => {
2475
  clearForm();
 
2476
  markSelected();
2477
  filter();
2478
  refresh();
2479
  };
2480
  $('applyImport').onclick = () => {
2481
  try {
2482
- applyObj(parseEnv($('importText').value), true);
2483
- showToast('Imported βœ“');
 
 
 
 
 
 
2484
  } catch (e) {
2485
  showToast('Import failed');
2486
  alert(e.message);
2487
  }
2488
  };
2489
 
2490
- // Auto-import: paste karo aur turant parse + apply ho jaata hai
2491
- $('importText').addEventListener('paste', () => {
2492
- setTimeout(() => {
2493
- try {
2494
- const val = $('importText').value.trim();
2495
- if (!val) return;
2496
- applyObj(parseEnv(val), true);
2497
- showToast('Auto-imported βœ“');
2498
- } catch (e) {
2499
- showToast('Import failed');
2500
- }
2501
- }, 0);
2502
- });
2503
-
2504
- // Live typing: jaise jaise type karo env format mein, bundle banta jaata hai
2505
- $('importText').addEventListener('input', () => {
2506
- const val = $('importText').value.trim();
2507
- if (!val) return;
2508
- // Sirf agar valid env/bundle format lag raha ho tabhi auto-apply
2509
- const looksLikeEnv = val.includes('=') || val.startsWith('{') || /^[A-Za-z0-9_\-]{20,}$/.test(val);
2510
- if (looksLikeEnv) {
2511
- try {
2512
- applyObj(parseEnv(val), true);
2513
- } catch (e) { /* silent β€” user abhi type kar raha hai */ }
2514
- }
2515
- });
2516
  $('addCustom').onclick = () => addCustomRow();
2517
  $('applyBundle').onclick = () => {
2518
  try {
@@ -2522,6 +2511,7 @@ $('applyBundle').onclick = () => {
2522
  showToast('Invalid bundle');
2523
  }
2524
  };
 
2525
  $('copyBundle').onclick = () => copyText($('bundleOut').value);
2526
  $('copyEnvLine').onclick = () => copyText($('envLineOut').value);
2527
  $('copyJson').onclick = () => copyText(JSON.stringify(collect(), null, 2));
 
458
  "g": "Core",
459
  "icon": "⚑",
460
  "k": "OPENCLAW_VERSION",
461
+ "lbl": "Pin OpenClaw version (build-time; rebuild required)",
462
  "type": "text",
463
  "ph": "latest",
464
+ "tag": "build"
465
  },
466
  {
467
  "g": "Plugins",
 
482
  "common": 1,
483
  "tag": "build"
484
  },
485
+ {
486
+ "g": "Startup",
487
+ "icon": "🩺",
488
+ "k": "AUTO_DOCTOR",
489
+ "lbl": "Auto-fix config on boot (openclaw doctor --fix)",
490
+ "type": "toggle",
491
+ "ph": "false",
492
+ "tag": "advanced"
493
+ },
494
  {
495
  "g": "Startup",
496
  "icon": "⚑",
 
851
  "g": "Core",
852
  "icon": "⚑",
853
  "k": "JUPYTER_TOKEN",
854
+ "lbl": "Jupyter access token (Must NOT be 'huggingface'. Run: openssl rand -hex 32)",
855
  "type": "password",
856
+ "ph": "change_this_to_a_strong_token",
857
  "common": 1,
858
  "tag": "credential"
859
  },
 
 
 
 
 
 
 
 
 
 
860
  {
861
  "g": "Core",
862
  "icon": "⚑",
 
965
  "ph": "/home/node",
966
  "tag": "advanced"
967
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
968
  {
969
  "g": "Provider Keys",
970
  "icon": "πŸ”‘",
 
2104
  return control;
2105
  }
2106
 
2107
+ function cardHTML(f, origIdx = 0) {
2108
  const TAG_META = {
2109
  critical: { cls: 'badge-critical', lbl: 'critical' },
2110
  credential: { cls: 'badge-credential', lbl: 'credential' },
 
2116
  const tm = TAG_META[f.tag] || TAG_META.optional;
2117
  const badge = `<span class="badge ${tm.cls}">${tm.lbl}</span>`;
2118
 
2119
+ return `<div class="env-card" data-row data-orig-idx="${origIdx}" data-group="${esc(f.g)}" data-search="${esc((f.g + ' ' + f.k + ' ' + (f.lbl || '') + ' ' + (f.tag || '')).toLowerCase())}">
2120
  <div class="card-top">
2121
  <input type="checkbox" class="card-check" data-check="${esc(f.k)}" ${f.common ? 'data-common="1"' : ''}>
2122
  <div class="card-info">
 
2194
  return obj;
2195
  }
2196
 
2197
+ function generateBundle() {
2198
  const obj = collect();
2199
  const keys = Object.keys(obj).sort();
2200
  const bundle = keys.length ? encodeBundle(Object.fromEntries(keys.map(k => [k, obj[k]]))) : '';
 
2201
  $('bundleOut').value = bundle;
2202
  $('envLineOut').value = bundle ? `HUGGINGCLAW_ENV_BUNDLE=${bundle}` : '';
2203
+ }
2204
 
2205
+ function refresh() {
2206
+ const obj = collect();
2207
+ const keys = Object.keys(obj).sort();
2208
  const s = $('summary');
2209
  if (keys.length) {
2210
  s.innerHTML = `<strong>${keys.length}</strong> variable${keys.length > 1 ? 's' : ''} selected<div class="sum-keys">${keys.map(k => `<span class="sum-key">${esc(k)}</span>`).join('')}</div>`;
 
2293
  addCustomRow(key, val, true);
2294
  }
2295
  }
2296
+ sortAllSections(); markSelected(); filter(); refresh();
2297
  }
2298
 
2299
  function autoCheck(key) {
 
2382
  const chk = document.querySelector(`[data-check="${CSS.escape(key)}"]`);
2383
  if (chk) {
2384
  chk.checked = on;
2385
+ sortSection(inp.closest('[data-row]'));
2386
  markSelected();
2387
  }
2388
  refresh();
2389
  }
2390
 
2391
+ function sortSection(cardEl) {
2392
+ const cards = cardEl && cardEl.closest('.cards');
2393
+ if (!cards) return;
2394
+ const all = [...cards.querySelectorAll('[data-row]')];
2395
+ const checked = all.filter(c => c.querySelector('[data-check]')?.checked);
2396
+ const rest = all.filter(c => !c.querySelector('[data-check]')?.checked);
2397
+ rest.sort((a, b) => Number(a.dataset.origIdx) - Number(b.dataset.origIdx));
2398
+ [...checked, ...rest].forEach(c => cards.appendChild(c));
2399
+ }
2400
+
2401
+ function sortAllSections() {
2402
+ document.querySelectorAll('.cards').forEach(cards => {
2403
+ const all = [...cards.querySelectorAll('[data-row]')];
2404
+ const checked = all.filter(c => c.querySelector('[data-check]')?.checked);
2405
+ const rest = all.filter(c => !c.querySelector('[data-check]')?.checked);
2406
+ rest.sort((a, b) => Number(a.dataset.origIdx) - Number(b.dataset.origIdx));
2407
+ [...checked, ...rest].forEach(c => cards.appendChild(c));
2408
+ });
2409
+ }
2410
+
2411
  function bindFieldEvents() {
2412
+ document.querySelectorAll('[data-check]').forEach(el => el.addEventListener('change', () => { sortSection(el.closest('[data-row]')); markSelected(); refresh(); }));
2413
  document.querySelectorAll('[data-key]').forEach(el => el.addEventListener('input', refresh));
2414
  document.querySelectorAll('[data-toggle]').forEach(btn => btn.addEventListener('click', () => toggleField(btn.dataset.toggle)));
2415
  document.querySelectorAll('[data-pick-for]').forEach(sel => sel.addEventListener('change', () => handlePickerChange(sel)));
 
2434
  <span class="sec-count">${items.length}</span>
2435
  <div class="sec-line"></div>
2436
  </div>
2437
+ <div class="cards">${items.map((f, i) => cardHTML(f, i)).join('')}</div>`;
2438
  wrap.appendChild(sec);
2439
  });
2440
  bindFieldEvents();
 
2468
  $('search').oninput = filter;
2469
  $('selectCommon').onclick = () => {
2470
  document.querySelectorAll('[data-common="1"]').forEach(c => c.checked = true);
2471
+ sortAllSections();
2472
  markSelected();
2473
  refresh();
2474
  };
2475
  $('selectVisible').onclick = () => {
2476
  document.querySelectorAll('.sec:not(.sec-hidden) [data-row]:not(.hidden) [data-check]').forEach(c => c.checked = true);
2477
+ sortAllSections();
2478
  markSelected();
2479
  refresh();
2480
  };
2481
  $('clearAll').onclick = () => {
2482
  clearForm();
2483
+ sortAllSections();
2484
  markSelected();
2485
  filter();
2486
  refresh();
2487
  };
2488
  $('applyImport').onclick = () => {
2489
  try {
2490
+ const parsed = parseEnv($('importText').value);
2491
+ const count = Object.keys(parsed).length;
2492
+ if (!count) {
2493
+ showToast('No valid env keys found');
2494
+ return;
2495
+ }
2496
+ applyObj(parsed, true);
2497
+ showToast(`Imported ${count} key${count > 1 ? 's' : ''} βœ“`);
2498
  } catch (e) {
2499
  showToast('Import failed');
2500
  alert(e.message);
2501
  }
2502
  };
2503
 
2504
+ // Import is explicit via the Import & Apply button to avoid surprising UI resets.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2505
  $('addCustom').onclick = () => addCustomRow();
2506
  $('applyBundle').onclick = () => {
2507
  try {
 
2511
  showToast('Invalid bundle');
2512
  }
2513
  };
2514
+ $('generateBundle').onclick = () => generateBundle();
2515
  $('copyBundle').onclick = () => copyText($('bundleOut').value);
2516
  $('copyEnvLine').onclick = () => copyText($('envLineOut').value);
2517
  $('copyJson').onclick = () => copyText(JSON.stringify(collect(), null, 2));
health-server.js CHANGED
@@ -72,7 +72,6 @@ if (_spacPrivacyEnv === "public") {
72
  SPACE_IS_PRIVATE = false;
73
  _privacyDetectionDone = true;
74
  console.log("[health-server] Space privacy: public (SPACE_PRIVACY env var override)");
75
- privacyDetectionReady.then ? void 0 : null;
76
  _privacyDetectionResolve && _privacyDetectionResolve();
77
  } else if (_spacPrivacyEnv === "private") {
78
  // User explicitly set SPACE_PRIVACY=private β€” skip API call entirely
 
72
  SPACE_IS_PRIVATE = false;
73
  _privacyDetectionDone = true;
74
  console.log("[health-server] Space privacy: public (SPACE_PRIVACY env var override)");
 
75
  _privacyDetectionResolve && _privacyDetectionResolve();
76
  } else if (_spacPrivacyEnv === "private") {
77
  // User explicitly set SPACE_PRIVACY=private β€” skip API call entirely
iframe-fix.cjs CHANGED
@@ -19,9 +19,10 @@ http.Server.prototype.emit = function (event, ...args) {
19
  if (event === "request") {
20
  const [, res] = args;
21
 
22
- // Only intercept on the main OpenClaw server (port 7860)
 
23
  const serverPort = this.address && this.address() && this.address().port;
24
- if (serverPort && serverPort !== 7860) {
25
  return origEmit.apply(this, [event, ...args]);
26
  }
27
 
 
19
  if (event === "request") {
20
  const [, res] = args;
21
 
22
+ // Only intercept on the main OpenClaw server (respects GATEWAY_PORT env var)
23
+ const expectedPort = Number(process.env.GATEWAY_PORT || 7860);
24
  const serverPort = this.address && this.address() && this.address().port;
25
+ if (serverPort && serverPort !== expectedPort) {
26
  return origEmit.apply(this, [event, ...args]);
27
  }
28
 
jupyter-devdata-sync.py CHANGED
@@ -174,6 +174,7 @@ def is_jupyter_running(port: int = 8888) -> bool:
174
  return False
175
 
176
  def restore_once(api, rid: str):
 
177
  from huggingface_hub.errors import RepositoryNotFoundError
178
  tmp = Path(tempfile.mkdtemp(prefix="devdata-restore-"))
179
  try:
 
174
  return False
175
 
176
  def restore_once(api, rid: str):
177
+ from huggingface_hub import snapshot_download
178
  from huggingface_hub.errors import RepositoryNotFoundError
179
  tmp = Path(tempfile.mkdtemp(prefix="devdata-restore-"))
180
  try:
multi-provider-key-rotator.cjs CHANGED
@@ -192,6 +192,12 @@ const PROVIDERS = [
192
  envPlural: 'MODELSTUDIO_API_KEYS',
193
  envSingular:'MODELSTUDIO_API_KEY',
194
  },
 
 
 
 
 
 
195
 
196
  ];
197
 
@@ -320,7 +326,25 @@ function patchFetch() {
320
  // Gemini: key URL query param mein jaata hai, Bearer nahi
321
  const url = new URL(typeof input === 'string' ? input : input.url);
322
  url.searchParams.set('key', key);
323
- input = typeof input === 'string' ? url.toString() : new Request(url.toString(), input);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
324
  } else {
325
  const headers = init.headers || (input && input.headers) || undefined;
326
  const patchedHeaders = setAuthHeader(headers, key);
 
192
  envPlural: 'MODELSTUDIO_API_KEYS',
193
  envSingular:'MODELSTUDIO_API_KEY',
194
  },
195
+ {
196
+ name: 'synthetic',
197
+ hostname: /(?:^|\.)synthetic\.local$/i,
198
+ envPlural: 'SYNTHETIC_API_KEYS',
199
+ envSingular: 'SYNTHETIC_API_KEY',
200
+ },
201
 
202
  ];
203
 
 
326
  // Gemini: key URL query param mein jaata hai, Bearer nahi
327
  const url = new URL(typeof input === 'string' ? input : input.url);
328
  url.searchParams.set('key', key);
329
+ if (typeof input === 'string') {
330
+ input = url.toString();
331
+ } else {
332
+ // Do NOT pass the Request object as init β€” that clones (consumes) the body stream.
333
+ // Instead patch only the URL via init object; fetch spec merges headers from Request.
334
+ init = {
335
+ method: input.method,
336
+ headers: input.headers,
337
+ body: input.body,
338
+ mode: input.mode,
339
+ credentials: input.credentials,
340
+ cache: input.cache,
341
+ redirect: input.redirect,
342
+ referrer: input.referrer,
343
+ integrity: input.integrity,
344
+ ...init,
345
+ };
346
+ input = url.toString();
347
+ }
348
  } else {
349
  const headers = init.headers || (input && input.headers) || undefined;
350
  const patchedHeaders = setAuthHeader(headers, key);
start.sh CHANGED
@@ -42,6 +42,9 @@ try:
42
  continue
43
  if str(key) in {"HUGGINGCLAW_ENV_BUNDLE", "ENV_BUNDLE"}:
44
  continue
 
 
 
45
  if os.environ.get(str(key), ""):
46
  continue
47
  if value is None or isinstance(value, (dict, list)):
@@ -99,6 +102,9 @@ DEVDATA_ENABLED=true
99
  if ! hc_is_true "$DEVDATA_NORMALIZED"; then
100
  DEVDATA_ENABLED=false
101
  fi
 
 
 
102
  if [ -n "${SPACE_HOST:-}" ]; then
103
  OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}"
104
  OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
@@ -353,8 +359,10 @@ CONFIG_JSON=$(jq \
353
  --arg fileLevel "$OPENCLAW_FILE_LOG_LEVEL" \
354
  --arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
355
  --arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
 
356
  '.gateway.auth.token = $token
357
  | .agents.defaults.model = $model
 
358
  | .logging.level = $fileLevel
359
  | .logging.consoleLevel = $consoleLevel
360
  | .logging.consoleStyle = $consoleStyle' <<<"$CONFIG_JSON")
@@ -367,7 +375,7 @@ CUSTOM_MODEL_NAME="${CUSTOM_MODEL_NAME:-$CUSTOM_MODEL_ID}"
367
  CUSTOM_API_KEY="${CUSTOM_API_KEY:-$LLM_API_KEY}"
368
  CUSTOM_API_TYPE="${CUSTOM_API_TYPE:-openai-completions}"
369
  CUSTOM_CONTEXT_WINDOW="${CUSTOM_CONTEXT_WINDOW:-128000}"
370
- CUSTOM_MAX_TOKENS="${CUSTOM_MAX_TOKENS:-500}"
371
 
372
  if [ -n "$CUSTOM_PROVIDER_NAME" ] || [ -n "$CUSTOM_BASE_URL" ] || [ -n "$CUSTOM_MODEL_ID" ]; then
373
  CUSTOM_PROVIDER_NORMALIZED=$(printf '%s' "$CUSTOM_PROVIDER_NAME" | tr '[:upper:]' '[:lower:]')
@@ -506,12 +514,24 @@ inject_provider_models_from_env "github-copilot" "GITHUB_COPILOT_MODELS" "COPILO
506
 
507
  # Browser configuration (managed local Chromium in HF/Docker)
508
  BROWSER_EXECUTABLE_PATH=""
509
- for candidate in /usr/bin/chromium /usr/bin/chromium-browser /snap/bin/chromium; do
 
 
 
 
 
 
 
 
 
510
  if [ -x "$candidate" ]; then
511
  BROWSER_EXECUTABLE_PATH="$candidate"
512
  break
513
  fi
514
  done
 
 
 
515
 
516
  BROWSER_SHOULD_ENABLE=false
517
  if [ "$BROWSER_PLUGIN_MODE" = "enabled" ] && [ -n "$BROWSER_EXECUTABLE_PATH" ] && [ -x "$BROWSER_EXECUTABLE_PATH" ]; then
@@ -569,7 +589,22 @@ if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then
569
  "defaultProfile": "openclaw",
570
  "headless": true,
571
  "noSandbox": true,
572
- "executablePath": $execPath
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
573
  }
574
  | .agents.defaults.sandbox.browser.allowHostControl = true' <<<"$CONFIG_JSON")
575
  fi
@@ -680,6 +715,10 @@ WHATSAPP_CONFIG_ENABLED=false
680
  if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
681
  WHATSAPP_CONFIG_ENABLED=true
682
  fi
 
 
 
 
683
  if [ -f "$EXISTING_CONFIG" ]; then
684
  echo "Restored config found β€” patching required fields and runtime channel/plugin toggles..."
685
  PATCHED=$(jq \
@@ -694,9 +733,11 @@ if [ -f "$EXISTING_CONFIG" ]; then
694
  --argjson consoleStyleConfigured "$OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED" \
695
  --argjson whatsappConfigured "$WHATSAPP_ENABLED_CONFIGURED" \
696
  --argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
 
697
  '(.channels.whatsapp // {}) as $existingWhatsapp
698
  | .gateway.auth.token = $token
699
  | .agents.defaults.model = $model
 
700
  | if $fileLogConfigured then .logging.level = $fileLevel else . end
701
  | if $consoleLogConfigured then .logging.consoleLevel = $consoleLevel else . end
702
  | if $consoleStyleConfigured then .logging.consoleStyle = $consoleStyle else . end
@@ -715,6 +756,13 @@ if [ -f "$EXISTING_CONFIG" ]; then
715
  | del(.channels.whatsapp)
716
  else
717
  .
 
 
 
 
 
 
 
718
  end' \
719
  "$EXISTING_CONFIG" 2>/dev/null)
720
 
@@ -758,7 +806,13 @@ fi
758
  if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
759
  echo "Proxy : ${CLOUDFLARE_PROXY_URL}"
760
  fi
761
- RUNTIME_JUPYTER_ENABLED="$DEV_MODE_ENABLED"
 
 
 
 
 
 
762
  # Add user bin to PATH for jupyter-lab (installed in Dockerfile when DEV_MODE=true)
763
  export PATH="$HOME/.local/bin:$PATH"
764
 
@@ -817,20 +871,24 @@ graceful_shutdown() {
817
  }
818
  trap graceful_shutdown SIGTERM SIGINT
819
 
 
820
  warmup_browser() {
821
  [ "$BROWSER_SHOULD_ENABLE" = "true" ] || return 0
 
 
 
822
 
823
  (
824
- sleep 5
825
 
826
  local attempt
827
- for attempt in 1 2 3 4 5; do
828
  if openclaw browser --browser-profile openclaw start >/dev/null 2>&1; then
829
  openclaw browser --browser-profile openclaw open about:blank >/dev/null 2>&1 || true
830
  echo "Managed browser ready."
831
  return 0
832
  fi
833
- sleep 2
834
  done
835
 
836
  echo "Warning: managed browser warm-up did not complete; first browser action may need a retry."
@@ -1438,7 +1496,9 @@ if [ -n "${HUGGINGCLAW_OPENCLAW_PLUGINS:-}" ]; then
1438
  fi
1439
 
1440
  # ── Fix config before running startup commands ──
1441
- openclaw doctor --fix || true
 
 
1442
 
1443
  # ── Arbitrary startup commands from HF Variables/Secrets ──
1444
  # Recommended: use one variable, HUGGINGCLAW_RUN, as a full bash script. If the
@@ -1556,6 +1616,16 @@ start_guardian_once() {
1556
  echo "WhatsApp Guardian started (PID: $GUARDIAN_PID)"
1557
  }
1558
 
 
 
 
 
 
 
 
 
 
 
1559
  while true; do
1560
  # Check health-server process - restart if died unexpectedly
1561
  if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then
@@ -1581,10 +1651,12 @@ while true; do
1581
  fi
1582
  fi
1583
 
1584
- openclaw doctor --fix || true
1585
- echo "Launching OpenClaw gateway on port 7860..."
 
 
1586
 
1587
- GATEWAY_ARGS=(gateway run --port 7860 --bind lan)
1588
  if [ "${GATEWAY_VERBOSE:-0}" = "1" ]; then
1589
  GATEWAY_ARGS+=(--verbose)
1590
  echo "Gateway verbose logging enabled (GATEWAY_VERBOSE=1)"
@@ -1597,13 +1669,13 @@ while true; do
1597
  stdbuf -oL -eL openclaw "${GATEWAY_ARGS[@]}" 2>&1 | tee -a /home/node/.openclaw/gateway.log &
1598
  GATEWAY_PID=$!
1599
 
1600
- # Poll for the gateway to start listening on 7860. OpenClaw can take 20-30s
1601
  # on cold start (plugin install + auto-restore). Bail out early if the
1602
  # pipeline died.
1603
  GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-90}"
1604
  ready=false
1605
  for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do
1606
- if (echo > /dev/tcp/127.0.0.1/7860) 2>/dev/null; then
1607
  ready=true
1608
  break
1609
  fi
@@ -1618,9 +1690,14 @@ while true; do
1618
  echo "Gateway failed to start. Last 30 lines of log:"
1619
  echo "────────────────────────────────────────────"
1620
  tail -30 /home/node/.openclaw/gateway.log
1621
- echo "Gateway failed β€” JupyterLab and env-builder still running. Retrying in 10s..."
1622
- sleep 10
1623
- continue
 
 
 
 
 
1624
  fi
1625
 
1626
  # 11. Start WhatsApp Guardian after the gateway is accepting connections
 
42
  continue
43
  if str(key) in {"HUGGINGCLAW_ENV_BUNDLE", "ENV_BUNDLE"}:
44
  continue
45
+ if str(key) == "OPENCLAW_VERSION":
46
+ print("Warning: OPENCLAW_VERSION from env bundle is ignored (build-time only; set HF Variable and rebuild).", file=sys.stderr)
47
+ continue
48
  if os.environ.get(str(key), ""):
49
  continue
50
  if value is None or isinstance(value, (dict, list)):
 
102
  if ! hc_is_true "$DEVDATA_NORMALIZED"; then
103
  DEVDATA_ENABLED=false
104
  fi
105
+ # On HF Spaces, browser is disabled by default (no display server).
106
+ # To enable: set BROWSER_PLUGIN_MODE=enabled as an HF Space secret.
107
+ # WARNING: requires at least CPU Upgrade tier (2 vCPU / 16GB RAM).
108
  if [ -n "${SPACE_HOST:-}" ]; then
109
  OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}"
110
  OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
 
359
  --arg fileLevel "$OPENCLAW_FILE_LOG_LEVEL" \
360
  --arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
361
  --arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
362
+ --arg port "$GATEWAY_PORT" \
363
  '.gateway.auth.token = $token
364
  | .agents.defaults.model = $model
365
+ | .gateway.port = ($port | tonumber)
366
  | .logging.level = $fileLevel
367
  | .logging.consoleLevel = $consoleLevel
368
  | .logging.consoleStyle = $consoleStyle' <<<"$CONFIG_JSON")
 
375
  CUSTOM_API_KEY="${CUSTOM_API_KEY:-$LLM_API_KEY}"
376
  CUSTOM_API_TYPE="${CUSTOM_API_TYPE:-openai-completions}"
377
  CUSTOM_CONTEXT_WINDOW="${CUSTOM_CONTEXT_WINDOW:-128000}"
378
+ CUSTOM_MAX_TOKENS="${CUSTOM_MAX_TOKENS:-8192}"
379
 
380
  if [ -n "$CUSTOM_PROVIDER_NAME" ] || [ -n "$CUSTOM_BASE_URL" ] || [ -n "$CUSTOM_MODEL_ID" ]; then
381
  CUSTOM_PROVIDER_NORMALIZED=$(printf '%s' "$CUSTOM_PROVIDER_NAME" | tr '[:upper:]' '[:lower:]')
 
514
 
515
  # Browser configuration (managed local Chromium in HF/Docker)
516
  BROWSER_EXECUTABLE_PATH=""
517
+ # On Debian/Ubuntu, /usr/bin/chromium is a shell wrapper; the real ELF binary
518
+ # lives at /usr/lib/chromium/chromium. Check the real binary first, then fall
519
+ # back to wrapper scripts (which are also valid executablePath values for
520
+ # Playwright/OpenClaw β€” they re-exec the real binary internally).
521
+ for candidate in \
522
+ /usr/lib/chromium/chromium \
523
+ /usr/lib/chromium-browser/chromium-browser \
524
+ /usr/bin/chromium \
525
+ /usr/bin/chromium-browser \
526
+ /snap/bin/chromium; do
527
  if [ -x "$candidate" ]; then
528
  BROWSER_EXECUTABLE_PATH="$candidate"
529
  break
530
  fi
531
  done
532
+ if [ -z "$BROWSER_EXECUTABLE_PATH" ]; then
533
+ echo "Warning: No real Chromium binary found. Browser plugin will be disabled."
534
+ fi
535
 
536
  BROWSER_SHOULD_ENABLE=false
537
  if [ "$BROWSER_PLUGIN_MODE" = "enabled" ] && [ -n "$BROWSER_EXECUTABLE_PATH" ] && [ -x "$BROWSER_EXECUTABLE_PATH" ]; then
 
589
  "defaultProfile": "openclaw",
590
  "headless": true,
591
  "noSandbox": true,
592
+ "executablePath": $execPath,
593
+ "localLaunchTimeoutMs": 45000,
594
+ "localCdpReadyTimeoutMs": 30000,
595
+ "extraArgs": [
596
+ "--no-sandbox",
597
+ "--disable-setuid-sandbox",
598
+ "--no-zygote",
599
+ "--disable-dev-shm-usage",
600
+ "--disable-gpu",
601
+ "--no-first-run",
602
+ "--disable-background-networking",
603
+ "--disable-sync",
604
+ "--disable-translate",
605
+ "--disable-notifications",
606
+ "--disable-speech-api"
607
+ ]
608
  }
609
  | .agents.defaults.sandbox.browser.allowHostControl = true' <<<"$CONFIG_JSON")
610
  fi
 
715
  if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
716
  WHATSAPP_CONFIG_ENABLED=true
717
  fi
718
+ TELEGRAM_CONFIG_ENABLED=false
719
+ if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
720
+ TELEGRAM_CONFIG_ENABLED=true
721
+ fi
722
  if [ -f "$EXISTING_CONFIG" ]; then
723
  echo "Restored config found β€” patching required fields and runtime channel/plugin toggles..."
724
  PATCHED=$(jq \
 
733
  --argjson consoleStyleConfigured "$OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED" \
734
  --argjson whatsappConfigured "$WHATSAPP_ENABLED_CONFIGURED" \
735
  --argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
736
+ --argjson telegramConfigured "$TELEGRAM_CONFIG_ENABLED" \
737
  '(.channels.whatsapp // {}) as $existingWhatsapp
738
  | .gateway.auth.token = $token
739
  | .agents.defaults.model = $model
740
+ | .gateway.port = ($desired.gateway.port // .gateway.port)
741
  | if $fileLogConfigured then .logging.level = $fileLevel else . end
742
  | if $consoleLogConfigured then .logging.consoleLevel = $consoleLevel else . end
743
  | if $consoleStyleConfigured then .logging.consoleStyle = $consoleStyle else . end
 
756
  | del(.channels.whatsapp)
757
  else
758
  .
759
+ end
760
+ | if $telegramConfigured then
761
+ .channels.telegram = (($desired.channels.telegram // {}) * (.channels.telegram // {}))
762
+ | .channels.telegram.botToken = $desired.channels.telegram.botToken
763
+ else
764
+ del(.channels.telegram)
765
+ | .plugins.entries.telegram.enabled = false
766
  end' \
767
  "$EXISTING_CONFIG" 2>/dev/null)
768
 
 
806
  if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
807
  echo "Proxy : ${CLOUDFLARE_PROXY_URL}"
808
  fi
809
+ # HUGGINGCLAW_JUPYTER_ENABLED env var se override allow karo
810
+ # (env-builder "Enable Jupyter terminal" toggle yahi set karta hai)
811
+ if hc_is_true "${HUGGINGCLAW_JUPYTER_ENABLED:-false}"; then
812
+ RUNTIME_JUPYTER_ENABLED=true
813
+ else
814
+ RUNTIME_JUPYTER_ENABLED="$DEV_MODE_ENABLED"
815
+ fi
816
  # Add user bin to PATH for jupyter-lab (installed in Dockerfile when DEV_MODE=true)
817
  export PATH="$HOME/.local/bin:$PATH"
818
 
 
871
  }
872
  trap graceful_shutdown SIGTERM SIGINT
873
 
874
+ BROWSER_WARMED_UP=false
875
  warmup_browser() {
876
  [ "$BROWSER_SHOULD_ENABLE" = "true" ] || return 0
877
+ # Only warm up once β€” gateway restarts should not re-spawn new warmup jobs.
878
+ [ "$BROWSER_WARMED_UP" = "false" ] || return 0
879
+ BROWSER_WARMED_UP=true
880
 
881
  (
882
+ sleep 8
883
 
884
  local attempt
885
+ for attempt in 1 2 3 4 5 6; do
886
  if openclaw browser --browser-profile openclaw start >/dev/null 2>&1; then
887
  openclaw browser --browser-profile openclaw open about:blank >/dev/null 2>&1 || true
888
  echo "Managed browser ready."
889
  return 0
890
  fi
891
+ sleep 5
892
  done
893
 
894
  echo "Warning: managed browser warm-up did not complete; first browser action may need a retry."
 
1496
  fi
1497
 
1498
  # ── Fix config before running startup commands ──
1499
+ if [ "${AUTO_DOCTOR:-false}" = "true" ]; then
1500
+ openclaw doctor --fix || true
1501
+ fi
1502
 
1503
  # ── Arbitrary startup commands from HF Variables/Secrets ──
1504
  # Recommended: use one variable, HUGGINGCLAW_RUN, as a full bash script. If the
 
1616
  echo "WhatsApp Guardian started (PID: $GUARDIAN_PID)"
1617
  }
1618
 
1619
+ # ── Start D-Bus session (once, before gateway loop) ──
1620
+ if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
1621
+ if command -v dbus-launch >/dev/null 2>&1; then
1622
+ eval "$(dbus-launch --sh-syntax 2>/dev/null)" || true
1623
+ export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-disabled:}"
1624
+ else
1625
+ export DBUS_SESSION_BUS_ADDRESS="disabled:"
1626
+ fi
1627
+ fi
1628
+
1629
  while true; do
1630
  # Check health-server process - restart if died unexpectedly
1631
  if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then
 
1651
  fi
1652
  fi
1653
 
1654
+ if [ "${AUTO_DOCTOR:-false}" = "true" ]; then
1655
+ openclaw doctor --fix || true
1656
+ fi
1657
+ echo "Launching OpenClaw gateway on port ${GATEWAY_PORT}..."
1658
 
1659
+ GATEWAY_ARGS=(gateway run --port "${GATEWAY_PORT}" --bind lan)
1660
  if [ "${GATEWAY_VERBOSE:-0}" = "1" ]; then
1661
  GATEWAY_ARGS+=(--verbose)
1662
  echo "Gateway verbose logging enabled (GATEWAY_VERBOSE=1)"
 
1669
  stdbuf -oL -eL openclaw "${GATEWAY_ARGS[@]}" 2>&1 | tee -a /home/node/.openclaw/gateway.log &
1670
  GATEWAY_PID=$!
1671
 
1672
+ # Poll for the gateway to start listening on ${GATEWAY_PORT}. OpenClaw can take 20-30s
1673
  # on cold start (plugin install + auto-restore). Bail out early if the
1674
  # pipeline died.
1675
  GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-90}"
1676
  ready=false
1677
  for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do
1678
+ if (echo > /dev/tcp/127.0.0.1/${GATEWAY_PORT}) 2>/dev/null; then
1679
  ready=true
1680
  break
1681
  fi
 
1690
  echo "Gateway failed to start. Last 30 lines of log:"
1691
  echo "────────────────────────────────────────────"
1692
  tail -30 /home/node/.openclaw/gateway.log
1693
+ if [ "$DEV_MODE_ENABLED" = "true" ]; then
1694
+ echo "Gateway failed β€” DEV_MODE active, retrying in 10s..."
1695
+ sleep 10
1696
+ continue
1697
+ else
1698
+ echo "Gateway failed β€” exiting."
1699
+ exit 1
1700
+ fi
1701
  fi
1702
 
1703
  # 11. Start WhatsApp Guardian after the gateway is accepting connections
wa-guardian.js CHANGED
@@ -17,7 +17,8 @@ try {
17
  }
18
  const { randomUUID } = require('node:crypto');
19
 
20
- const GATEWAY_URL = "ws://127.0.0.1:7860";
 
21
  const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
22
  const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
23
  const CHECK_INTERVAL = 30000;
@@ -125,7 +126,8 @@ async function createConnection() {
125
  });
126
  }
127
 
128
- async function callRpc(ws, method, params) {
 
129
  return new Promise((resolve, reject) => {
130
  const id = randomUUID();
131
  const handler = (data) => {
@@ -148,7 +150,7 @@ async function callRpc(ws, method, params) {
148
  reject(sendErr);
149
  return;
150
  }
151
- setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, WAIT_TIMEOUT + 5000);
152
  });
153
  }
154
 
@@ -188,7 +190,7 @@ async function checkStatus() {
188
  }
189
 
190
  console.log("[guardian] Waiting for pairing completion...");
191
- const waitRes = await callRpc(ws, "web.login.wait", { timeoutMs: WAIT_TIMEOUT });
192
  const result = waitRes.payload || waitRes.result;
193
  const message = result?.message || "";
194
  const linkedAfter515 = !result?.connected && message.includes("515");
@@ -202,19 +204,27 @@ async function checkStatus() {
202
  lastConnectedAt = Date.now();
203
  writeStatus({ configured: true, connected: true, pairing: false });
204
 
 
 
 
 
205
  if (linkedAfter515) {
206
  console.log("[guardian] 515 after scan: credentials saved, reloading config to start WhatsApp...");
207
  } else {
208
  console.log("[guardian] Pairing completed! Reloading config...");
209
  }
210
 
211
- const getRes = await callRpc(ws, "config.get", {});
212
- if (getRes.payload?.raw && getRes.payload?.hash) {
213
- await callRpc(ws, "config.apply", { raw: getRes.payload.raw, baseHash: getRes.payload.hash });
214
- console.log("[guardian] Configuration re-applied.");
 
 
 
 
 
 
215
  }
216
-
217
- shouldStop = true;
218
  setTimeout(() => process.exit(0), 1000);
219
  } else if (!message.includes("No active") && !message.includes("Still waiting")) {
220
  console.log(`[guardian] Wait result: ${message}`);
 
17
  }
18
  const { randomUUID } = require('node:crypto');
19
 
20
+ const GATEWAY_PORT = Number.parseInt(process.env.GATEWAY_PORT || "7860", 10);
21
+ const GATEWAY_URL = `ws://127.0.0.1:${GATEWAY_PORT}`;
22
  const GATEWAY_TOKEN = process.env.GATEWAY_TOKEN || "huggingclaw";
23
  const WHATSAPP_ENABLED = /^true$/i.test(process.env.WHATSAPP_ENABLED || "");
24
  const CHECK_INTERVAL = 30000;
 
126
  });
127
  }
128
 
129
+ async function callRpc(ws, method, params, timeoutMs) {
130
+ const ms = timeoutMs !== undefined ? timeoutMs : 10000; // default 10s for normal calls
131
  return new Promise((resolve, reject) => {
132
  const id = randomUUID();
133
  const handler = (data) => {
 
150
  reject(sendErr);
151
  return;
152
  }
153
+ setTimeout(() => { ws.removeListener("message", handler); reject(new Error("RPC Timeout")); }, ms);
154
  });
155
  }
156
 
 
190
  }
191
 
192
  console.log("[guardian] Waiting for pairing completion...");
193
+ const waitRes = await callRpc(ws, "web.login.wait", { timeoutMs: WAIT_TIMEOUT }, WAIT_TIMEOUT + 5000);
194
  const result = waitRes.payload || waitRes.result;
195
  const message = result?.message || "";
196
  const linkedAfter515 = !result?.connected && message.includes("515");
 
204
  lastConnectedAt = Date.now();
205
  writeStatus({ configured: true, connected: true, pairing: false });
206
 
207
+ // Set shouldStop BEFORE config.apply β€” gateway restart during apply must not
208
+ // leave guardian running (it would then incorrectly wipe valid credentials).
209
+ shouldStop = true;
210
+
211
  if (linkedAfter515) {
212
  console.log("[guardian] 515 after scan: credentials saved, reloading config to start WhatsApp...");
213
  } else {
214
  console.log("[guardian] Pairing completed! Reloading config...");
215
  }
216
 
217
+ try {
218
+ const getRes = await callRpc(ws, "config.get", {});
219
+ if (getRes.payload?.raw && getRes.payload?.hash) {
220
+ await callRpc(ws, "config.apply", { raw: getRes.payload.raw, baseHash: getRes.payload.hash });
221
+ console.log("[guardian] Configuration re-applied.");
222
+ }
223
+ } catch (applyErr) {
224
+ // Gateway restarted during config.apply β€” that is expected and fine.
225
+ // shouldStop is already true so we will not retry or attempt logout.
226
+ console.log(`[guardian] Config re-apply interrupted (gateway restarting): ${applyErr.message}`);
227
  }
 
 
228
  setTimeout(() => process.exit(0), 1000);
229
  } else if (!message.includes("No active") && !message.includes("Still waiting")) {
230
  console.log(`[guardian] Wait result: ${message}`);