Anurag commited on
Commit
21a9e26
·
1 Parent(s): 1d367af

Harden Chromium launch flags for headless HF container stability

Browse files
Files changed (4) hide show
  1. Dockerfile +1 -0
  2. README.md +8 -16
  3. health-server.js +5 -2
  4. start.sh +72 -10
Dockerfile CHANGED
@@ -21,6 +21,7 @@ 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 \
 
21
  RUN apt-get update && apt-get install -y \
22
  git \
23
  sudo \
24
+ file \
25
  ca-certificates \
26
  jq \
27
  curl \
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 strong token for the JupyterLab terminal at /terminal/ (defaults to huggingface)."
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
- - 💻 **Optional Dev Terminal:** JupyterLab is available at `/terminal/` only when `DEV_MODE=true` (disabled by default).
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
- 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,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 (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
-
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 only when `DEV_MODE=true`).
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
 
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";
@@ -634,7 +637,7 @@ const server = http.createServer(async (req, res) => {
634
  if (pathname === JUPYTER_BASE || pathname.startsWith(JUPYTER_BASE + "/")) {
635
  if (!JUPYTER_ENABLED) {
636
  res.writeHead(404, { "Content-Type": "application/json" });
637
- return res.end(JSON.stringify({ status: "disabled", message: "JupyterLab terminal is disabled. Set DEV_MODE=true to enable /terminal/." }));
638
  }
639
  if (isDirectHfSpaceRequest) {
640
  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";
 
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" });
start.sh CHANGED
@@ -93,6 +93,12 @@ DEV_MODE_ENABLED=false
93
  if hc_is_true "$DEV_MODE_NORMALIZED"; then
94
  DEV_MODE_ENABLED=true
95
  fi
 
 
 
 
 
 
96
  SYNC_INTERVAL="$(trim_var "${SYNC_INTERVAL:-180}")"
97
  DEVDATA_DATASET_NAME="$(trim_var "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}")"
98
  DEVDATA_SYNC_INTERVAL="$(trim_var "${DEVDATA_SYNC_INTERVAL:-180}")"
@@ -514,10 +520,44 @@ inject_provider_models_from_env "github-copilot" "GITHUB_COPILOT_MODELS" "COPILO
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 \
@@ -525,14 +565,29 @@ for candidate in \
525
  /usr/bin/chromium-browser \
526
  /snap/bin/chromium; do
527
  if [ -x "$candidate" ]; then
528
- if file "$candidate" 2>/dev/null | grep -q "ELF"; then
 
 
 
 
 
 
529
  BROWSER_EXECUTABLE_PATH="$candidate"
530
  break
531
  fi
 
 
 
532
  fi
533
  done
 
 
 
 
 
 
534
  if [ -z "$BROWSER_EXECUTABLE_PATH" ]; then
535
- echo "Warning: No real Chromium binary found. Browser plugin will be disabled."
536
  fi
537
 
538
  BROWSER_SHOULD_ENABLE=false
@@ -585,21 +640,21 @@ CONFIG_JSON=$(jq \
585
 
586
  if [ "$BROWSER_SHOULD_ENABLE" = "true" ]; then
587
  CONFIG_JSON=$(jq \
588
- --arg execPath "$BROWSER_EXECUTABLE_PATH" \
589
  '.browser = {
590
  "enabled": true,
591
  "defaultProfile": "openclaw",
592
  "headless": true,
593
  "noSandbox": true,
594
- "executablePath": $execPath,
595
- "localLaunchTimeoutMs": 45000,
596
- "localCdpReadyTimeoutMs": 30000,
597
  "extraArgs": [
 
598
  "--no-sandbox",
599
  "--disable-setuid-sandbox",
600
  "--no-zygote",
601
  "--disable-dev-shm-usage",
602
  "--disable-gpu",
 
 
 
603
  "--no-first-run",
604
  "--disable-background-networking",
605
  "--disable-sync",
@@ -925,6 +980,13 @@ start_jupyter_once() {
925
  return 0
926
  fi
927
 
 
 
 
 
 
 
 
928
  # Security guard: refuse to start JupyterLab with the insecure default token.
929
  # JupyterLab exposes a full shell — a weak token is equivalent to no auth.
930
  if [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; then
 
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}")"
 
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 \
 
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
 
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",
 
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