Spaces:
Running
Running
Merge pull request #106 from anurag008w/edit
Browse filesEnhance startup robustness, Jupyter security, and browser reliability
- .env.example +2 -9
- Dockerfile +3 -0
- README.md +8 -16
- env-builder.html +2 -1
- env-builder.js +57 -67
- health-server.js +5 -3
- iframe-fix.cjs +3 -2
- jupyter-devdata-sync.py +1 -0
- multi-provider-key-rotator.cjs +25 -1
- start.sh +171 -20
- wa-guardian.js +20 -10
.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 |
-
#
|
| 252 |
-
JUPYTER_TOKEN=
|
| 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
|
@@ -21,9 +21,12 @@ ENV DEV_MODE=${DEV_MODE}
|
|
| 21 |
RUN apt-get update && apt-get install -y \
|
| 22 |
git \
|
| 23 |
sudo \
|
|
|
|
| 24 |
ca-certificates \
|
| 25 |
jq \
|
| 26 |
curl \
|
|
|
|
|
|
|
| 27 |
python3 \
|
| 28 |
python3-pip \
|
| 29 |
chromium \
|
|
|
|
| 21 |
RUN apt-get update && apt-get install -y \
|
| 22 |
git \
|
| 23 |
sudo \
|
| 24 |
+
file \
|
| 25 |
ca-certificates \
|
| 26 |
jq \
|
| 27 |
curl \
|
| 28 |
+
dbus \
|
| 29 |
+
dbus-x11 \
|
| 30 |
python3 \
|
| 31 |
python3-pip \
|
| 32 |
chromium \
|
README.md
CHANGED
|
@@ -20,7 +20,7 @@ secrets:
|
|
| 20 |
- name: GATEWAY_TOKEN
|
| 21 |
description: "Strong token to secure your OpenClaw Control UI (generate: openssl rand -hex 32)."
|
| 22 |
- name: JUPYTER_TOKEN
|
| 23 |
-
description: "Optional
|
| 24 |
- name: CLOUDFLARE_WORKERS_TOKEN
|
| 25 |
description: "Cloudflare API token β auto-creates a Worker proxy and KeepAlive monitor."
|
| 26 |
- name: TELEGRAM_ALLOWED_USERS
|
|
@@ -57,7 +57,6 @@ secrets:
|
|
| 57 |
- [π» Local Development](#-local-development)
|
| 58 |
- [π CLI Access](#-cli-access)
|
| 59 |
- [π» JupyterLab Terminal](#-jupyterlab-terminal)
|
| 60 |
-
- [π Merge Comparison](#-merge-comparison)
|
| 61 |
- [ποΈ Architecture](#-architecture)
|
| 62 |
- [π Staying Alive](#-staying-alive)
|
| 63 |
- [π Troubleshooting](#-troubleshooting)
|
|
@@ -78,7 +77,7 @@ secrets:
|
|
| 78 |
- π **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
|
| 79 |
- π **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
|
| 80 |
- π **Flexible Auth:** Secure the Control UI with either a gateway token or password.
|
| 81 |
-
- π» **
|
| 82 |
- π **100% HF-Native:** Runs entirely on HuggingFaceβs free infrastructure (2 vCPU, 16GB RAM).
|
| 83 |
|
| 84 |
## π₯ Video Tutorial
|
|
@@ -104,7 +103,9 @@ 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 |
-
|
|
|
|
|
|
|
| 108 |
|
| 109 |
### Step 3: Deploy & Run
|
| 110 |
|
|
@@ -366,21 +367,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
|
| 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 |
-
>
|
| 375 |
-
|
| 376 |
-
## π Merge Comparison
|
| 377 |
-
|
| 378 |
-
This repository is a merge of two sources:
|
| 379 |
-
|
| 380 |
-
- `anurag162008/HuggingClaw`: OpenClaw gateway, dashboard, Cloudflare proxy/keep-alive, Telegram/WhatsApp helpers, backup sync, key rotation, docs, and security metadata.
|
| 381 |
-
- Hugging Face `SpacesExamples/jupyterlab` template: JupyterLab Docker behavior, token login UX, Hugging Face-branded login template, pinned Jupyter packages, and Git LFS defaults for large model/data artifacts.
|
| 382 |
-
|
| 383 |
-
The main merge-specific change is the single-port router: HF Spaces exposes `7861`, while the router keeps OpenClaw at `/app/` and JupyterLab at `/terminal/` without leaking internal redirects such as `http://127.0.0.1:8888/...`.
|
| 384 |
|
| 385 |
## ποΈ Architecture
|
| 386 |
|
|
@@ -402,7 +394,7 @@ HuggingClaw uses a multi-layered approach to ensure stability and persistence on
|
|
| 402 |
2. Resolve backup namespace and restore workspace from HF Dataset.
|
| 403 |
3. Generate `openclaw.json` configuration.
|
| 404 |
4. Launch background tasks (auto-sync, channel helpers).
|
| 405 |
-
5. Start the local dashboard/reverse proxy and OpenClaw gateway (JupyterLab starts
|
| 406 |
|
| 407 |
</details>
|
| 408 |
|
|
|
|
| 20 |
- name: GATEWAY_TOKEN
|
| 21 |
description: "Strong token to secure your OpenClaw Control UI (generate: openssl rand -hex 32)."
|
| 22 |
- name: JUPYTER_TOKEN
|
| 23 |
+
description: "Optional token for the JupyterLab terminal at /terminal/. Defaults to GATEWAY_TOKEN when set β no extra secret needed."
|
| 24 |
- name: CLOUDFLARE_WORKERS_TOKEN
|
| 25 |
description: "Cloudflare API token β auto-creates a Worker proxy and KeepAlive monitor."
|
| 26 |
- name: TELEGRAM_ALLOWED_USERS
|
|
|
|
| 57 |
- [π» Local Development](#-local-development)
|
| 58 |
- [π CLI Access](#-cli-access)
|
| 59 |
- [π» JupyterLab Terminal](#-jupyterlab-terminal)
|
|
|
|
| 60 |
- [ποΈ Architecture](#-architecture)
|
| 61 |
- [π Staying Alive](#-staying-alive)
|
| 62 |
- [π Troubleshooting](#-troubleshooting)
|
|
|
|
| 77 |
- π **Visual Dashboard:** Beautiful Web UI to monitor uptime, sync status, and active models.
|
| 78 |
- π **Webhooks:** Get notified on restarts or backup failures via standard webhooks.
|
| 79 |
- π **Flexible Auth:** Secure the Control UI with either a gateway token or password.
|
| 80 |
+
- π» **Terminal Out of the Box:** JupyterLab is available at `/terminal/` automatically when `GATEWAY_TOKEN` is set β no extra config needed. `GATEWAY_TOKEN` is reused as the terminal auth token. Set `DEV_MODE=false` explicitly to opt out.
|
| 81 |
- π **100% HF-Native:** Runs entirely on HuggingFaceβs free infrastructure (2 vCPU, 16GB RAM).
|
| 82 |
|
| 83 |
## π₯ Video Tutorial
|
|
|
|
| 103 |
> [!TIP]
|
| 104 |
> HuggingClaw is completely flexible! You only need these three secrets to get started. You can set other secrets later.
|
| 105 |
|
| 106 |
+
**Terminal auto-enables when `GATEWAY_TOKEN` is set** β no extra secrets needed. `GATEWAY_TOKEN` is reused as `JUPYTER_TOKEN`, so the terminal is protected by the same credential as the Control UI. To set a different token, add `JUPYTER_TOKEN` as a Secret. To disable the terminal entirely, set `DEV_MODE=false` as a Variable.
|
| 107 |
+
|
| 108 |
+
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).
|
| 109 |
|
| 110 |
### Step 3: Deploy & Run
|
| 111 |
|
|
|
|
| 367 |
| :--- | :--- | :--- | :--- |
|
| 368 |
| `/` | HuggingClaw dashboard | `7861` | Public HF Spaces entrypoint |
|
| 369 |
| `/app/` | OpenClaw Control UI | `7860` | Mounted behind the local reverse proxy |
|
| 370 |
+
| `/terminal/` | JupyterLab terminal | `8888` | Auto-enabled when `GATEWAY_TOKEN` is set; uses `GATEWAY_TOKEN` as auth token unless `JUPYTER_TOKEN` is set separately. Set `DEV_MODE=false` to disable. |
|
| 371 |
|
| 372 |
When enabled, the terminal notebook root is `/home/node`, so you can inspect HuggingClaw files, logs, workspace state, and runtime scripts from the browser.
|
| 373 |
|
| 374 |
> [!IMPORTANT]
|
| 375 |
+
> No extra secret needed β `GATEWAY_TOKEN` is automatically reused as `JUPYTER_TOKEN`. Set a separate `JUPYTER_TOKEN` secret only if you want a different terminal credential.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 376 |
|
| 377 |
## ποΈ Architecture
|
| 378 |
|
|
|
|
| 394 |
2. Resolve backup namespace and restore workspace from HF Dataset.
|
| 395 |
3. Generate `openclaw.json` configuration.
|
| 396 |
4. Launch background tasks (auto-sync, channel helpers).
|
| 397 |
+
5. Start the local dashboard/reverse proxy and OpenClaw gateway (JupyterLab starts when `GATEWAY_TOKEN` is set, `DEV_MODE=true`, or `HUGGINGCLAW_JUPYTER_ENABLED=true`).
|
| 398 |
|
| 399 |
</details>
|
| 400 |
|
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 |
-
<
|
|
|
|
| 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": "
|
| 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": "
|
| 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
|
| 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 |
-
|
| 2483 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2484 |
} catch (e) {
|
| 2485 |
showToast('Import failed');
|
| 2486 |
alert(e.message);
|
| 2487 |
}
|
| 2488 |
};
|
| 2489 |
|
| 2490 |
-
//
|
| 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
|
@@ -20,9 +20,12 @@ const GATEWAY_HOST = "127.0.0.1";
|
|
| 20 |
const JUPYTER_PORT = Number.parseInt(process.env.JUPYTER_PORT || "8888", 10);
|
| 21 |
const JUPYTER_HOST = "127.0.0.1";
|
| 22 |
const JUPYTER_BASE = normalizeBase(process.env.JUPYTER_BASE, "/terminal");
|
|
|
|
| 23 |
const DEV_MODE_ENABLED = isTrue(process.env.DEV_MODE);
|
|
|
|
|
|
|
| 24 |
const JUPYTER_ENABLED = /^(true|1|yes|on)$/i.test(
|
| 25 |
-
process.env.HUGGINGCLAW_JUPYTER_ENABLED || (DEV_MODE_ENABLED ? "true" : "false")
|
| 26 |
);
|
| 27 |
const startTime = Date.now();
|
| 28 |
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
|
|
@@ -72,7 +75,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
|
|
@@ -635,7 +637,7 @@ const server = http.createServer(async (req, res) => {
|
|
| 635 |
if (pathname === JUPYTER_BASE || pathname.startsWith(JUPYTER_BASE + "/")) {
|
| 636 |
if (!JUPYTER_ENABLED) {
|
| 637 |
res.writeHead(404, { "Content-Type": "application/json" });
|
| 638 |
-
return res.end(JSON.stringify({ status: "disabled", message: "JupyterLab terminal is disabled. Set DEV_MODE=true to enable /terminal/." }));
|
| 639 |
}
|
| 640 |
if (isDirectHfSpaceRequest) {
|
| 641 |
res.writeHead(200, { "Content-Type": "text/html" });
|
|
|
|
| 20 |
const JUPYTER_PORT = Number.parseInt(process.env.JUPYTER_PORT || "8888", 10);
|
| 21 |
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 |
+
// Auto-enable Jupyter when DEV_MODE=true, HUGGINGCLAW_JUPYTER_ENABLED=true, or GATEWAY_TOKEN is set.
|
| 26 |
+
// GATEWAY_TOKEN doubles as JUPYTER_TOKEN in start.sh β no extra secret needed.
|
| 27 |
const JUPYTER_ENABLED = /^(true|1|yes|on)$/i.test(
|
| 28 |
+
process.env.HUGGINGCLAW_JUPYTER_ENABLED || (DEV_MODE_ENABLED ? "true" : GATEWAY_TOKEN ? "true" : "false")
|
| 29 |
);
|
| 30 |
const startTime = Date.now();
|
| 31 |
const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
|
|
|
|
| 75 |
SPACE_IS_PRIVATE = false;
|
| 76 |
_privacyDetectionDone = true;
|
| 77 |
console.log("[health-server] Space privacy: public (SPACE_PRIVACY env var override)");
|
|
|
|
| 78 |
_privacyDetectionResolve && _privacyDetectionResolve();
|
| 79 |
} else if (_spacPrivacyEnv === "private") {
|
| 80 |
// User explicitly set SPACE_PRIVACY=private β skip API call entirely
|
|
|
|
| 637 |
if (pathname === JUPYTER_BASE || pathname.startsWith(JUPYTER_BASE + "/")) {
|
| 638 |
if (!JUPYTER_ENABLED) {
|
| 639 |
res.writeHead(404, { "Content-Type": "application/json" });
|
| 640 |
+
return res.end(JSON.stringify({ status: "disabled", message: "JupyterLab terminal is disabled. Set GATEWAY_TOKEN or DEV_MODE=true to enable /terminal/ (or set HUGGINGCLAW_JUPYTER_ENABLED=true)." }));
|
| 641 |
}
|
| 642 |
if (isDirectHfSpaceRequest) {
|
| 643 |
res.writeHead(200, { "Content-Type": "text/html" });
|
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 (
|
|
|
|
| 23 |
const serverPort = this.address && this.address() && this.address().port;
|
| 24 |
-
if (serverPort && serverPort !==
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)):
|
|
@@ -90,6 +93,12 @@ DEV_MODE_ENABLED=false
|
|
| 90 |
if hc_is_true "$DEV_MODE_NORMALIZED"; then
|
| 91 |
DEV_MODE_ENABLED=true
|
| 92 |
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
SYNC_INTERVAL="$(trim_var "${SYNC_INTERVAL:-180}")"
|
| 94 |
DEVDATA_DATASET_NAME="$(trim_var "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}")"
|
| 95 |
DEVDATA_SYNC_INTERVAL="$(trim_var "${DEVDATA_SYNC_INTERVAL:-180}")"
|
|
@@ -99,6 +108,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}"
|
|
@@ -320,7 +332,7 @@ CONFIG_JSON=$(cat <<'CONFIGEOF'
|
|
| 320 |
{
|
| 321 |
"gateway": {
|
| 322 |
"mode": "local",
|
| 323 |
-
"port":
|
| 324 |
"bind": "lan",
|
| 325 |
"auth": {
|
| 326 |
"token": ""
|
|
@@ -353,8 +365,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 +381,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:-
|
| 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 +520,75 @@ 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 510 |
if [ -x "$candidate" ]; then
|
| 511 |
-
|
| 512 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
|
@@ -563,13 +640,28 @@ CONFIG_JSON=$(jq \
|
|
| 563 |
|
| 564 |
if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then
|
| 565 |
CONFIG_JSON=$(jq \
|
| 566 |
-
--arg execPath "$BROWSER_EXECUTABLE_PATH" \
|
| 567 |
'.browser = {
|
| 568 |
"enabled": true,
|
| 569 |
"defaultProfile": "openclaw",
|
| 570 |
"headless": true,
|
| 571 |
"noSandbox": true,
|
| 572 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 573 |
}
|
| 574 |
| .agents.defaults.sandbox.browser.allowHostControl = true' <<<"$CONFIG_JSON")
|
| 575 |
fi
|
|
@@ -680,6 +772,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 +790,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 +813,13 @@ if [ -f "$EXISTING_CONFIG" ]; then
|
|
| 715 |
| del(.channels.whatsapp)
|
| 716 |
else
|
| 717 |
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 718 |
end' \
|
| 719 |
"$EXISTING_CONFIG" 2>/dev/null)
|
| 720 |
|
|
@@ -758,7 +863,13 @@ fi
|
|
| 758 |
if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
|
| 759 |
echo "Proxy : ${CLOUDFLARE_PROXY_URL}"
|
| 760 |
fi
|
| 761 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 +928,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
|
| 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
|
| 834 |
done
|
| 835 |
|
| 836 |
echo "Warning: managed browser warm-up did not complete; first browser action may need a retry."
|
|
@@ -865,6 +980,13 @@ start_jupyter_once() {
|
|
| 865 |
return 0
|
| 866 |
fi
|
| 867 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 868 |
# Security guard: refuse to start JupyterLab with the insecure default token.
|
| 869 |
# JupyterLab exposes a full shell β a weak token is equivalent to no auth.
|
| 870 |
if [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; then
|
|
@@ -1438,7 +1560,9 @@ if [ -n "${HUGGINGCLAW_OPENCLAW_PLUGINS:-}" ]; then
|
|
| 1438 |
fi
|
| 1439 |
|
| 1440 |
# ββ Fix config before running startup commands ββ
|
| 1441 |
-
|
|
|
|
|
|
|
| 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 +1680,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 +1715,12 @@ while true; do
|
|
| 1581 |
fi
|
| 1582 |
fi
|
| 1583 |
|
| 1584 |
-
|
| 1585 |
-
|
|
|
|
|
|
|
| 1586 |
|
| 1587 |
-
GATEWAY_ARGS=(gateway run --port
|
| 1588 |
if [ "${GATEWAY_VERBOSE:-0}" = "1" ]; then
|
| 1589 |
GATEWAY_ARGS+=(--verbose)
|
| 1590 |
echo "Gateway verbose logging enabled (GATEWAY_VERBOSE=1)"
|
|
@@ -1597,13 +1733,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
|
| 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/
|
| 1607 |
ready=true
|
| 1608 |
break
|
| 1609 |
fi
|
|
@@ -1618,14 +1754,29 @@ 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 |
-
|
| 1622 |
-
|
| 1623 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1624 |
fi
|
| 1625 |
|
| 1626 |
# 11. Start WhatsApp Guardian after the gateway is accepting connections
|
| 1627 |
start_guardian_once
|
| 1628 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1629 |
# 11.5 Warm up the managed browser so first browser actions have a live tab
|
| 1630 |
warmup_browser
|
| 1631 |
|
|
|
|
| 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)):
|
|
|
|
| 93 |
if hc_is_true "$DEV_MODE_NORMALIZED"; then
|
| 94 |
DEV_MODE_ENABLED=true
|
| 95 |
fi
|
| 96 |
+
# Auto-enable DEV_MODE when GATEWAY_TOKEN is set and DEV_MODE was not explicitly configured.
|
| 97 |
+
# GATEWAY_TOKEN doubles as JUPYTER_TOKEN (see start_jupyter_once) β no extra secret required.
|
| 98 |
+
if [ "$DEV_MODE_ENABLED" != "true" ] && [ -z "${DEV_MODE:-}" ] && [ -n "${GATEWAY_TOKEN:-}" ]; then
|
| 99 |
+
DEV_MODE_ENABLED=true
|
| 100 |
+
echo "GATEWAY_TOKEN set and DEV_MODE not explicitly configured β auto-enabling terminal (set DEV_MODE=false to opt out)"
|
| 101 |
+
fi
|
| 102 |
SYNC_INTERVAL="$(trim_var "${SYNC_INTERVAL:-180}")"
|
| 103 |
DEVDATA_DATASET_NAME="$(trim_var "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}")"
|
| 104 |
DEVDATA_SYNC_INTERVAL="$(trim_var "${DEVDATA_SYNC_INTERVAL:-180}")"
|
|
|
|
| 108 |
if ! hc_is_true "$DEVDATA_NORMALIZED"; then
|
| 109 |
DEVDATA_ENABLED=false
|
| 110 |
fi
|
| 111 |
+
# On HF Spaces, browser is disabled by default (no display server).
|
| 112 |
+
# To enable: set BROWSER_PLUGIN_MODE=enabled as an HF Space secret.
|
| 113 |
+
# WARNING: requires at least CPU Upgrade tier (2 vCPU / 16GB RAM).
|
| 114 |
if [ -n "${SPACE_HOST:-}" ]; then
|
| 115 |
OPENCLAW_CONSOLE_LOG_LEVEL="${OPENCLAW_CONSOLE_LOG_LEVEL:-warn}"
|
| 116 |
OPENCLAW_FILE_LOG_LEVEL="${OPENCLAW_FILE_LOG_LEVEL:-info}"
|
|
|
|
| 332 |
{
|
| 333 |
"gateway": {
|
| 334 |
"mode": "local",
|
| 335 |
+
"port": "${GATEWAY_PORT}",
|
| 336 |
"bind": "lan",
|
| 337 |
"auth": {
|
| 338 |
"token": ""
|
|
|
|
| 365 |
--arg fileLevel "$OPENCLAW_FILE_LOG_LEVEL" \
|
| 366 |
--arg consoleLevel "$OPENCLAW_CONSOLE_LOG_LEVEL" \
|
| 367 |
--arg consoleStyle "$OPENCLAW_CONSOLE_LOG_STYLE" \
|
| 368 |
+
--arg port "$GATEWAY_PORT" \
|
| 369 |
'.gateway.auth.token = $token
|
| 370 |
| .agents.defaults.model = $model
|
| 371 |
+
| .gateway.port = ($port | tonumber)
|
| 372 |
| .logging.level = $fileLevel
|
| 373 |
| .logging.consoleLevel = $consoleLevel
|
| 374 |
| .logging.consoleStyle = $consoleStyle' <<<"$CONFIG_JSON")
|
|
|
|
| 381 |
CUSTOM_API_KEY="${CUSTOM_API_KEY:-$LLM_API_KEY}"
|
| 382 |
CUSTOM_API_TYPE="${CUSTOM_API_TYPE:-openai-completions}"
|
| 383 |
CUSTOM_CONTEXT_WINDOW="${CUSTOM_CONTEXT_WINDOW:-128000}"
|
| 384 |
+
CUSTOM_MAX_TOKENS="${CUSTOM_MAX_TOKENS:-8192}"
|
| 385 |
|
| 386 |
if [ -n "$CUSTOM_PROVIDER_NAME" ] || [ -n "$CUSTOM_BASE_URL" ] || [ -n "$CUSTOM_MODEL_ID" ]; then
|
| 387 |
CUSTOM_PROVIDER_NORMALIZED=$(printf '%s' "$CUSTOM_PROVIDER_NAME" | tr '[:upper:]' '[:lower:]')
|
|
|
|
| 520 |
|
| 521 |
# Browser configuration (managed local Chromium in HF/Docker)
|
| 522 |
BROWSER_EXECUTABLE_PATH=""
|
| 523 |
+
BROWSER_WRAPPER_PATH=""
|
| 524 |
+
HAS_FILE_CMD=false
|
| 525 |
+
if command -v file >/dev/null 2>&1; then
|
| 526 |
+
HAS_FILE_CMD=true
|
| 527 |
+
fi
|
| 528 |
+
|
| 529 |
+
ensure_chromium_for_browser_plugin() {
|
| 530 |
+
# Enforce Chromium availability when browser plugin is explicitly enabled.
|
| 531 |
+
[ "$BROWSER_PLUGIN_MODE" = "enabled" ] || return 0
|
| 532 |
+
for candidate in /usr/lib/chromium/chromium /usr/bin/chromium /usr/bin/chromium-browser; do
|
| 533 |
+
[ -x "$candidate" ] && return 0
|
| 534 |
+
done
|
| 535 |
+
if [ "$HAS_FILE_CMD" != "true" ]; then
|
| 536 |
+
echo "BROWSER_PLUGIN_MODE=enabled and 'file' command is missing; attempting runtime install..."
|
| 537 |
+
if _hc_apt_install file; then
|
| 538 |
+
HAS_FILE_CMD=true
|
| 539 |
+
echo "'file' command installed via apt-get."
|
| 540 |
+
else
|
| 541 |
+
echo "Warning: could not install 'file'; continuing with executable-path fallback checks."
|
| 542 |
+
fi
|
| 543 |
+
fi
|
| 544 |
+
echo "BROWSER_PLUGIN_MODE=enabled but Chromium is missing; attempting runtime install..."
|
| 545 |
+
if _hc_apt_install chromium; then
|
| 546 |
+
echo "Chromium installed via apt-get."
|
| 547 |
+
return 0
|
| 548 |
+
fi
|
| 549 |
+
if _hc_apt_install chromium-browser; then
|
| 550 |
+
echo "Chromium browser package installed via apt-get."
|
| 551 |
+
return 0
|
| 552 |
+
fi
|
| 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
|
| 559 |
+
# ELF binary lives under /usr/lib/chromium/*. Prefer a real ELF binary, then
|
| 560 |
+
# fall back to wrapper launchers (Playwright/OpenClaw can execute those too).
|
| 561 |
+
for candidate in \
|
| 562 |
+
/usr/lib/chromium/chromium \
|
| 563 |
+
/usr/lib/chromium-browser/chromium-browser \
|
| 564 |
+
/usr/bin/chromium \
|
| 565 |
+
/usr/bin/chromium-browser \
|
| 566 |
+
/snap/bin/chromium; do
|
| 567 |
if [ -x "$candidate" ]; then
|
| 568 |
+
if [ "$HAS_FILE_CMD" = "true" ]; then
|
| 569 |
+
if file "$candidate" 2>/dev/null | grep -q "ELF"; then
|
| 570 |
+
BROWSER_EXECUTABLE_PATH="$candidate"
|
| 571 |
+
break
|
| 572 |
+
fi
|
| 573 |
+
else
|
| 574 |
+
# Minimal images may not ship `file`; accept the first executable path.
|
| 575 |
+
BROWSER_EXECUTABLE_PATH="$candidate"
|
| 576 |
+
break
|
| 577 |
+
fi
|
| 578 |
+
if [ -z "$BROWSER_WRAPPER_PATH" ]; then
|
| 579 |
+
BROWSER_WRAPPER_PATH="$candidate"
|
| 580 |
+
fi
|
| 581 |
fi
|
| 582 |
done
|
| 583 |
+
if [ -z "$BROWSER_EXECUTABLE_PATH" ] && [ -n "$BROWSER_WRAPPER_PATH" ]; then
|
| 584 |
+
BROWSER_EXECUTABLE_PATH="$BROWSER_WRAPPER_PATH"
|
| 585 |
+
echo "No ELF Chromium binary found; using launcher wrapper at $BROWSER_EXECUTABLE_PATH"
|
| 586 |
+
elif [ -n "$BROWSER_EXECUTABLE_PATH" ] && [ "$HAS_FILE_CMD" != "true" ]; then
|
| 587 |
+
echo "Detected Chromium executable at $BROWSER_EXECUTABLE_PATH (ELF probe skipped: 'file' command not installed)"
|
| 588 |
+
fi
|
| 589 |
+
if [ -z "$BROWSER_EXECUTABLE_PATH" ]; then
|
| 590 |
+
echo "Warning: Chromium executable not found. Browser plugin will be disabled."
|
| 591 |
+
fi
|
| 592 |
|
| 593 |
BROWSER_SHOULD_ENABLE=false
|
| 594 |
if [ "$BROWSER_PLUGIN_MODE" = "enabled" ] && [ -n "$BROWSER_EXECUTABLE_PATH" ] && [ -x "$BROWSER_EXECUTABLE_PATH" ]; then
|
|
|
|
| 640 |
|
| 641 |
if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then
|
| 642 |
CONFIG_JSON=$(jq \
|
|
|
|
| 643 |
'.browser = {
|
| 644 |
"enabled": true,
|
| 645 |
"defaultProfile": "openclaw",
|
| 646 |
"headless": true,
|
| 647 |
"noSandbox": true,
|
| 648 |
+
"extraArgs": [
|
| 649 |
+
"--headless=new",
|
| 650 |
+
"--no-sandbox",
|
| 651 |
+
"--disable-setuid-sandbox",
|
| 652 |
+
"--no-zygote",
|
| 653 |
+
"--disable-dev-shm-usage",
|
| 654 |
+
"--disable-gpu",
|
| 655 |
+
"--remote-debugging-address=127.0.0.1",
|
| 656 |
+
"--disable-features=UseDBus,MediaRouter",
|
| 657 |
+
"--password-store=basic",
|
| 658 |
+
"--no-first-run",
|
| 659 |
+
"--disable-background-networking",
|
| 660 |
+
"--disable-sync",
|
| 661 |
+
"--disable-translate",
|
| 662 |
+
"--disable-notifications",
|
| 663 |
+
"--disable-speech-api"
|
| 664 |
+
]
|
| 665 |
}
|
| 666 |
| .agents.defaults.sandbox.browser.allowHostControl = true' <<<"$CONFIG_JSON")
|
| 667 |
fi
|
|
|
|
| 772 |
if [ "$WHATSAPP_ENABLED_NORMALIZED" = "true" ]; then
|
| 773 |
WHATSAPP_CONFIG_ENABLED=true
|
| 774 |
fi
|
| 775 |
+
TELEGRAM_CONFIG_ENABLED=false
|
| 776 |
+
if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then
|
| 777 |
+
TELEGRAM_CONFIG_ENABLED=true
|
| 778 |
+
fi
|
| 779 |
if [ -f "$EXISTING_CONFIG" ]; then
|
| 780 |
echo "Restored config found β patching required fields and runtime channel/plugin toggles..."
|
| 781 |
PATCHED=$(jq \
|
|
|
|
| 790 |
--argjson consoleStyleConfigured "$OPENCLAW_CONSOLE_LOG_STYLE_CONFIGURED" \
|
| 791 |
--argjson whatsappConfigured "$WHATSAPP_ENABLED_CONFIGURED" \
|
| 792 |
--argjson whatsappEnabled "$WHATSAPP_CONFIG_ENABLED" \
|
| 793 |
+
--argjson telegramConfigured "$TELEGRAM_CONFIG_ENABLED" \
|
| 794 |
'(.channels.whatsapp // {}) as $existingWhatsapp
|
| 795 |
| .gateway.auth.token = $token
|
| 796 |
| .agents.defaults.model = $model
|
| 797 |
+
| .gateway.port = ($desired.gateway.port // .gateway.port)
|
| 798 |
| if $fileLogConfigured then .logging.level = $fileLevel else . end
|
| 799 |
| if $consoleLogConfigured then .logging.consoleLevel = $consoleLevel else . end
|
| 800 |
| if $consoleStyleConfigured then .logging.consoleStyle = $consoleStyle else . end
|
|
|
|
| 813 |
| del(.channels.whatsapp)
|
| 814 |
else
|
| 815 |
.
|
| 816 |
+
end
|
| 817 |
+
| if $telegramConfigured then
|
| 818 |
+
.channels.telegram = (($desired.channels.telegram // {}) * (.channels.telegram // {}))
|
| 819 |
+
| .channels.telegram.botToken = $desired.channels.telegram.botToken
|
| 820 |
+
else
|
| 821 |
+
del(.channels.telegram)
|
| 822 |
+
| .plugins.entries.telegram.enabled = false
|
| 823 |
end' \
|
| 824 |
"$EXISTING_CONFIG" 2>/dev/null)
|
| 825 |
|
|
|
|
| 863 |
if [ -n "${CLOUDFLARE_PROXY_URL:-}" ]; then
|
| 864 |
echo "Proxy : ${CLOUDFLARE_PROXY_URL}"
|
| 865 |
fi
|
| 866 |
+
# HUGGINGCLAW_JUPYTER_ENABLED env var se override allow karo
|
| 867 |
+
# (env-builder "Enable Jupyter terminal" toggle yahi set karta hai)
|
| 868 |
+
if hc_is_true "${HUGGINGCLAW_JUPYTER_ENABLED:-false}"; then
|
| 869 |
+
RUNTIME_JUPYTER_ENABLED=true
|
| 870 |
+
else
|
| 871 |
+
RUNTIME_JUPYTER_ENABLED="$DEV_MODE_ENABLED"
|
| 872 |
+
fi
|
| 873 |
# Add user bin to PATH for jupyter-lab (installed in Dockerfile when DEV_MODE=true)
|
| 874 |
export PATH="$HOME/.local/bin:$PATH"
|
| 875 |
|
|
|
|
| 928 |
}
|
| 929 |
trap graceful_shutdown SIGTERM SIGINT
|
| 930 |
|
| 931 |
+
BROWSER_WARMED_UP=false
|
| 932 |
warmup_browser() {
|
| 933 |
[ "$BROWSER_SHOULD_ENABLE" = "true" ] || return 0
|
| 934 |
+
# Only warm up once β gateway restarts should not re-spawn new warmup jobs.
|
| 935 |
+
[ "$BROWSER_WARMED_UP" = "false" ] || return 0
|
| 936 |
+
BROWSER_WARMED_UP=true
|
| 937 |
|
| 938 |
(
|
| 939 |
+
sleep 8
|
| 940 |
|
| 941 |
local attempt
|
| 942 |
+
for attempt in 1 2 3 4 5 6; do
|
| 943 |
if openclaw browser --browser-profile openclaw start >/dev/null 2>&1; then
|
| 944 |
openclaw browser --browser-profile openclaw open about:blank >/dev/null 2>&1 || true
|
| 945 |
echo "Managed browser ready."
|
| 946 |
return 0
|
| 947 |
fi
|
| 948 |
+
sleep 5
|
| 949 |
done
|
| 950 |
|
| 951 |
echo "Warning: managed browser warm-up did not complete; first browser action may need a retry."
|
|
|
|
| 980 |
return 0
|
| 981 |
fi
|
| 982 |
|
| 983 |
+
# GATEWAY_TOKEN fallback: if JUPYTER_TOKEN is unset or still the insecure default,
|
| 984 |
+
# reuse GATEWAY_TOKEN. Both protect the same Space, so the credential is equivalent.
|
| 985 |
+
if { [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; } && [ -n "${GATEWAY_TOKEN:-}" ]; then
|
| 986 |
+
JUPYTER_TOKEN="$GATEWAY_TOKEN"
|
| 987 |
+
echo "JUPYTER_TOKEN not set β using GATEWAY_TOKEN as terminal auth token"
|
| 988 |
+
fi
|
| 989 |
+
|
| 990 |
# Security guard: refuse to start JupyterLab with the insecure default token.
|
| 991 |
# JupyterLab exposes a full shell β a weak token is equivalent to no auth.
|
| 992 |
if [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; then
|
|
|
|
| 1560 |
fi
|
| 1561 |
|
| 1562 |
# ββ Fix config before running startup commands ββ
|
| 1563 |
+
if [ "${AUTO_DOCTOR:-false}" = "true" ]; then
|
| 1564 |
+
openclaw doctor --fix || true
|
| 1565 |
+
fi
|
| 1566 |
|
| 1567 |
# ββ Arbitrary startup commands from HF Variables/Secrets ββ
|
| 1568 |
# Recommended: use one variable, HUGGINGCLAW_RUN, as a full bash script. If the
|
|
|
|
| 1680 |
echo "WhatsApp Guardian started (PID: $GUARDIAN_PID)"
|
| 1681 |
}
|
| 1682 |
|
| 1683 |
+
# ββ Start D-Bus session (once, before gateway loop) ββ
|
| 1684 |
+
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
|
| 1685 |
+
if command -v dbus-launch >/dev/null 2>&1; then
|
| 1686 |
+
eval "$(dbus-launch --sh-syntax 2>/dev/null)" || true
|
| 1687 |
+
export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-disabled:}"
|
| 1688 |
+
else
|
| 1689 |
+
export DBUS_SESSION_BUS_ADDRESS="disabled:"
|
| 1690 |
+
fi
|
| 1691 |
+
fi
|
| 1692 |
+
|
| 1693 |
while true; do
|
| 1694 |
# Check health-server process - restart if died unexpectedly
|
| 1695 |
if [ -n "${HEALTH_PID:-}" ] && ! kill -0 "$HEALTH_PID" 2>/dev/null; then
|
|
|
|
| 1715 |
fi
|
| 1716 |
fi
|
| 1717 |
|
| 1718 |
+
if [ "${AUTO_DOCTOR:-false}" = "true" ]; then
|
| 1719 |
+
openclaw doctor --fix || true
|
| 1720 |
+
fi
|
| 1721 |
+
echo "Launching OpenClaw gateway on port ${GATEWAY_PORT}..."
|
| 1722 |
|
| 1723 |
+
GATEWAY_ARGS=(gateway run --port "${GATEWAY_PORT}" --bind lan)
|
| 1724 |
if [ "${GATEWAY_VERBOSE:-0}" = "1" ]; then
|
| 1725 |
GATEWAY_ARGS+=(--verbose)
|
| 1726 |
echo "Gateway verbose logging enabled (GATEWAY_VERBOSE=1)"
|
|
|
|
| 1733 |
stdbuf -oL -eL openclaw "${GATEWAY_ARGS[@]}" 2>&1 | tee -a /home/node/.openclaw/gateway.log &
|
| 1734 |
GATEWAY_PID=$!
|
| 1735 |
|
| 1736 |
+
# Poll for the gateway to start listening on ${GATEWAY_PORT}. OpenClaw can take 20-30s
|
| 1737 |
# on cold start (plugin install + auto-restore). Bail out early if the
|
| 1738 |
# pipeline died.
|
| 1739 |
GATEWAY_READY_TIMEOUT="${GATEWAY_READY_TIMEOUT:-90}"
|
| 1740 |
ready=false
|
| 1741 |
for ((i=0; i<GATEWAY_READY_TIMEOUT; i++)); do
|
| 1742 |
+
if (echo > /dev/tcp/127.0.0.1/${GATEWAY_PORT}) 2>/dev/null; then
|
| 1743 |
ready=true
|
| 1744 |
break
|
| 1745 |
fi
|
|
|
|
| 1754 |
echo "Gateway failed to start. Last 30 lines of log:"
|
| 1755 |
echo "ββββββββββββββββββββββββββββββββββββββββββββ"
|
| 1756 |
tail -30 /home/node/.openclaw/gateway.log
|
| 1757 |
+
if [ "$DEV_MODE_ENABLED" = "true" ]; then
|
| 1758 |
+
echo "Gateway failed β DEV_MODE active, retrying in 10s..."
|
| 1759 |
+
sleep 10
|
| 1760 |
+
continue
|
| 1761 |
+
else
|
| 1762 |
+
echo "Gateway failed β exiting."
|
| 1763 |
+
exit 1
|
| 1764 |
+
fi
|
| 1765 |
fi
|
| 1766 |
|
| 1767 |
# 11. Start WhatsApp Guardian after the gateway is accepting connections
|
| 1768 |
start_guardian_once
|
| 1769 |
|
| 1770 |
+
# ββ Silence D-Bus errors for headless Chromium ββ
|
| 1771 |
+
if [ -z "${DBUS_SESSION_BUS_ADDRESS:-}" ]; then
|
| 1772 |
+
if command -v dbus-launch >/dev/null 2>&1; then
|
| 1773 |
+
eval "$(dbus-launch --sh-syntax 2>/dev/null)" || true
|
| 1774 |
+
export DBUS_SESSION_BUS_ADDRESS="${DBUS_SESSION_BUS_ADDRESS:-disabled:}"
|
| 1775 |
+
else
|
| 1776 |
+
export DBUS_SESSION_BUS_ADDRESS="disabled:"
|
| 1777 |
+
fi
|
| 1778 |
+
fi
|
| 1779 |
+
|
| 1780 |
# 11.5 Warm up the managed browser so first browser actions have a live tab
|
| 1781 |
warmup_browser
|
| 1782 |
|
wa-guardian.js
CHANGED
|
@@ -17,7 +17,8 @@ try {
|
|
| 17 |
}
|
| 18 |
const { randomUUID } = require('node:crypto');
|
| 19 |
|
| 20 |
-
const
|
|
|
|
| 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")); },
|
| 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 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}`);
|