Anurag commited on
Commit
2be260d
Β·
unverified Β·
2 Parent(s): 2b974b98ba914e

Merge branch 'main' into edit

Browse files
Files changed (7) hide show
  1. .dockerignore +12 -0
  2. Dockerfile +5 -3
  3. README.md +5 -4
  4. env-builder.js +8 -9
  5. health-server.js +96 -7
  6. login.html +12 -22
  7. start.sh +11 -17
.dockerignore ADDED
@@ -0,0 +1,12 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ .git
2
+ .DS_Store
3
+ __pycache__
4
+ *.pyc
5
+ node_modules
6
+ .env
7
+ .env.*
8
+ !.env.example
9
+ venv
10
+ .venv
11
+ tasks/
12
+ *.log
Dockerfile CHANGED
@@ -15,10 +15,12 @@ FROM ghcr.io/openclaw/openclaw:${OPENCLAW_VERSION} AS openclaw
15
  FROM node:22-slim
16
  ARG OPENCLAW_VERSION=latest
17
  ARG DEV_MODE=false
18
- ENV DEV_MODE=${DEV_MODE}
 
 
19
 
20
  # Install system dependencies (+ optional JupyterLab deps in DEV_MODE)
21
- RUN apt-get update && apt-get install -y \
22
  git \
23
  sudo \
24
  file \
@@ -51,7 +53,7 @@ RUN apt-get update && apt-get install -y \
51
  fonts-wqy-zenhei \
52
  xfonts-scalable \
53
  --no-install-recommends && \
54
- pip3 install --no-cache-dir --break-system-packages huggingface_hub && \
55
  rm -rf /var/lib/apt/lists/*
56
 
57
  # Install JupyterLab only when DEV_MODE is enabled (build-time)
 
15
  FROM node:22-slim
16
  ARG OPENCLAW_VERSION=latest
17
  ARG DEV_MODE=false
18
+ # DEV_MODE intentionally not baked into runtime ENV β€” defaults to unset so
19
+ # start.sh can auto-enable terminal when GATEWAY_TOKEN is present. Users can
20
+ # override by setting DEV_MODE=false as an HF Space Variable to opt out.
21
 
22
  # Install system dependencies (+ optional JupyterLab deps in DEV_MODE)
23
+ RUN apt-get update && apt-get install -y --no-install-recommends \
24
  git \
25
  sudo \
26
  file \
 
53
  fonts-wqy-zenhei \
54
  xfonts-scalable \
55
  --no-install-recommends && \
56
+ pip3 install --no-cache-dir --break-system-packages huggingface_hub hf_transfer && \
57
  rm -rf /var/lib/apt/lists/*
58
 
59
  # Install JupyterLab only when DEV_MODE is enabled (build-time)
README.md CHANGED
@@ -118,7 +118,7 @@ HuggingClaw features a built-in dashboard to track:
118
  - **Uptime:** Real-time uptime monitoring.
119
  - **Sync Status:** Visual indicators for workspace backup operations.
120
  - **Chat Status:** Real-time connection status for WhatsApp and Telegram.
121
- - **Model Info:** See which LLM is currently powering your assistant.
122
 
123
  ## πŸ“± Telegram Setup *(Optional)*
124
 
@@ -133,6 +133,7 @@ To chat via Telegram:
133
  | :--- | :--- | :--- |
134
  | `TELEGRAM_BOT_TOKEN` | β€” | Telegram bot token from BotFather |
135
  | `TELEGRAM_ALLOWED_USERS` | β€” | Comma-separated Telegram user IDs for access |
 
136
 
137
  ## 🌐 Cloudflare Proxy Setup
138
 
@@ -381,9 +382,9 @@ HuggingClaw uses a multi-layered approach to ensure stability and persistence on
381
  <details>
382
  <summary><b>Click to view technical details</b></summary>
383
 
384
- - **Dashboard (`/`)**: Management, monitoring, and keep-alive tools (terminal controls appear only in DEV mode).
385
  - **Control UI (`/app/`)**: Secure interface for managing agents and channels, proxied to the OpenClaw gateway on internal port `7860`.
386
- - **JupyterLab Terminal (`/terminal/`)**: Browser terminal/notebook server on internal port `8888` (DEV mode only).
387
  - **Health Check (`/health`)**: Endpoint for uptime monitoring and readiness probes.
388
  - **Sync Engine**: Python background process managing HF Dataset persistence.
389
  - **Transparent Proxy**: Interceptor for requests to blocked domains (Telegram, etc.).
@@ -394,7 +395,7 @@ HuggingClaw uses a multi-layered approach to ensure stability and persistence on
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
 
 
118
  - **Uptime:** Real-time uptime monitoring.
119
  - **Sync Status:** Visual indicators for workspace backup operations.
120
  - **Chat Status:** Real-time connection status for WhatsApp and Telegram.
121
+ - **Model Info:** See which LLM and provider are currently powering your assistant.
122
 
123
  ## πŸ“± Telegram Setup *(Optional)*
124
 
 
133
  | :--- | :--- | :--- |
134
  | `TELEGRAM_BOT_TOKEN` | β€” | Telegram bot token from BotFather |
135
  | `TELEGRAM_ALLOWED_USERS` | β€” | Comma-separated Telegram user IDs for access |
136
+ | `TELEGRAM_WEBHOOK_URL` | *(auto-provisioned)* | Override webhook URL; set `TELEGRAM_MODE=polling` to use long-polling instead |
137
 
138
  ## 🌐 Cloudflare Proxy Setup
139
 
 
382
  <details>
383
  <summary><b>Click to view technical details</b></summary>
384
 
385
+ - **Dashboard (`/`)**: Management, monitoring, and keep-alive tools. Terminal button appears when DEV mode is enabled (default when `GATEWAY_TOKEN` is set).
386
  - **Control UI (`/app/`)**: Secure interface for managing agents and channels, proxied to the OpenClaw gateway on internal port `7860`.
387
+ - **JupyterLab Terminal (`/terminal/`)**: Browser terminal/notebook server on internal port `8888` (auto-enabled when `GATEWAY_TOKEN` is set; set `DEV_MODE=false` to disable).
388
  - **Health Check (`/health`)**: Endpoint for uptime monitoring and readiness probes.
389
  - **Sync Engine**: Python background process managing HF Dataset persistence.
390
  - **Transparent Proxy**: Interceptor for requests to blocked domains (Telegram, etc.).
 
395
  2. Resolve backup namespace and restore workspace from HF Dataset.
396
  3. Generate `openclaw.json` configuration.
397
  4. Launch background tasks (auto-sync, channel helpers).
398
+ 5. Start the local dashboard/reverse proxy and OpenClaw gateway (JupyterLab starts automatically when `GATEWAY_TOKEN` is set; set `DEV_MODE=false` to opt out).
399
 
400
  </details>
401
 
env-builder.js CHANGED
@@ -644,11 +644,10 @@ const FIELDS = [
644
  "k": "WEBHOOK_URL",
645
  "lbl": "Webhook URL",
646
  "type": "text",
647
- "ph": "https://...",
648
- "tag": "feature"
649
  },
650
- {
651
- "g": "Gateway",
652
  "icon": "⚑",
653
  "k": "GATEWAY_MAX_RESTARTS",
654
  "lbl": "Gateway max restarts",
@@ -853,11 +852,11 @@ const FIELDS = [
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": "⚑",
863
  "k": "OPENCLAW_DISABLE_BONJOUR",
@@ -965,7 +964,7 @@ const FIELDS = [
965
  "ph": "/home/node",
966
  "tag": "advanced"
967
  },
968
- {
969
  "g": "Provider Keys",
970
  "icon": "πŸ”‘",
971
  "k": "ANTHROPIC_API_KEY",
 
644
  "k": "WEBHOOK_URL",
645
  "lbl": "Webhook URL",
646
  "type": "text",
647
+ "ph": "https://..."
 
648
  },
649
+ {
650
+ "g": "Core",
651
  "icon": "⚑",
652
  "k": "GATEWAY_MAX_RESTARTS",
653
  "lbl": "Gateway max restarts",
 
852
  "k": "JUPYTER_TOKEN",
853
  "lbl": "Jupyter access token (Must NOT be 'huggingface'. Run: openssl rand -hex 32)",
854
  "type": "password",
855
+ "secret": 1,
856
+ "ph": "huggingface",
857
+ "common": 1
858
  },
859
+ {
860
  "g": "Core",
861
  "icon": "⚑",
862
  "k": "OPENCLAW_DISABLE_BONJOUR",
 
964
  "ph": "/home/node",
965
  "tag": "advanced"
966
  },
967
+ {
968
  "g": "Provider Keys",
969
  "icon": "πŸ”‘",
970
  "k": "ANTHROPIC_API_KEY",
health-server.js CHANGED
@@ -22,13 +22,14 @@ 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";
 
 
32
  const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
33
  const WHATSAPP_ENABLED = isTrue(process.env.WHATSAPP_ENABLED);
34
  const WHATSAPP_STATUS_FILE = "/tmp/huggingclaw-wa-status.json";
@@ -240,6 +241,65 @@ function escapeHtml(v) {
240
  return String(v).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
241
  }
242
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  function badge(label, tone = "neutral") {
244
  return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
245
  }
@@ -262,9 +322,9 @@ function renderDashboard(data) {
262
 
263
  const tiles = [
264
  tile({ title: "Gateway", value: badge(data.gatewayReady ? "Online" : "Offline", data.gatewayReady ? "ok" : "off"), detail: `OpenClaw on internal port ${GATEWAY_PORT}`, tone: data.gatewayReady ? "ok" : "off" }),
265
- tile({ title: "Model", value: `<code>${escapeHtml(LLM_MODEL)}</code>`, detail: "Primary LLM configured", tone: "neutral" }),
266
  tile({ title: "Runtime", value: escapeHtml(data.uptimeHuman), detail: `Public port ${PORT}`, tone: "neutral" }),
267
- tile({ title: "Telegram", value: badge(TELEGRAM_ENABLED ? "Enabled" : "Disabled", TELEGRAM_ENABLED ? "ok" : "neutral"), detail: TELEGRAM_ENABLED ? "Bot channel active" : "Not configured", tone: TELEGRAM_ENABLED ? "ok" : "neutral" }),
268
  ];
269
 
270
 
@@ -599,16 +659,45 @@ const server = http.createServer(async (req, res) => {
599
  !isSameOriginNav &&
600
  !isFromHFApp;
601
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
602
  if (pathname === "/env-builder" || pathname === "/env-builder/") {
603
  if (isDirectHfSpaceRequest) {
604
  res.writeHead(200, { "Content-Type": "text/html" });
605
  return res.end(renderPrivateRedirect(HF_SPACE_URL));
606
  }
 
 
 
 
607
  res.writeHead(200, { "Content-Type": "text/html" });
608
  return res.end(renderEnvBuilder());
609
  }
610
 
611
  if (pathname === "/env-builder.js") {
 
 
 
 
612
  try {
613
  const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
614
  res.writeHead(200, { "Content-Type": "application/javascript" });
 
22
  const JUPYTER_BASE = normalizeBase(process.env.JUPYTER_BASE, "/terminal");
23
  const GATEWAY_TOKEN = (process.env.GATEWAY_TOKEN || "").trim();
24
  const DEV_MODE_ENABLED = isTrue(process.env.DEV_MODE);
25
+ // Default true. Only false when DEV_MODE=false or HUGGINGCLAW_JUPYTER_ENABLED=false is explicitly set.
26
+ const JUPYTER_ENABLED =
27
+ !/^(false|0|no|off)$/i.test(String(process.env.DEV_MODE || "").trim()) &&
28
+ !/^(false|0|no|off)$/i.test(String(process.env.HUGGINGCLAW_JUPYTER_ENABLED || "").trim());
 
29
  const startTime = Date.now();
30
  const LLM_MODEL = process.env.LLM_MODEL || "Not Set";
31
+ const LLM_PROVIDER = LLM_MODEL.includes("/") ? LLM_MODEL.split("/")[0] : "";
32
+ const TELEGRAM_WEBHOOK_URL = (process.env.TELEGRAM_WEBHOOK_URL || "").trim();
33
  const TELEGRAM_ENABLED = !!process.env.TELEGRAM_BOT_TOKEN;
34
  const WHATSAPP_ENABLED = isTrue(process.env.WHATSAPP_ENABLED);
35
  const WHATSAPP_STATUS_FILE = "/tmp/huggingclaw-wa-status.json";
 
241
  return String(v).replace(/&/g,"&amp;").replace(/</g,"&lt;").replace(/>/g,"&gt;").replace(/"/g,"&quot;");
242
  }
243
 
244
+ function parseCookies(req) {
245
+ const h = req.headers.cookie || "";
246
+ return Object.fromEntries(h.split(";").map(c => c.trim().split("=")).filter(p => p.length >= 2).map(([k, ...v]) => [k.trim(), decodeURIComponent(v.join("=").trim())]));
247
+ }
248
+
249
+ // Constant-time comparison β€” prevent timing attacks on token check
250
+ function safeEqual(a, b) {
251
+ if (typeof a !== "string" || typeof b !== "string" || a.length !== b.length) return false;
252
+ let d = 0;
253
+ for (let i = 0; i < a.length; i++) d |= a.charCodeAt(i) ^ b.charCodeAt(i);
254
+ return d === 0;
255
+ }
256
+
257
+ function isEnvBuilderAuthed(req) {
258
+ if (!GATEWAY_TOKEN) return true; // unprotected when no token set
259
+ return safeEqual(parseCookies(req).hc_env_auth || "", GATEWAY_TOKEN);
260
+ }
261
+
262
+ function readBody(req) {
263
+ return new Promise((resolve) => {
264
+ let body = "";
265
+ req.on("data", chunk => { body += chunk; if (body.length > 4096) { body = ""; req.destroy(); } });
266
+ req.on("end", () => resolve(body));
267
+ req.on("error", () => resolve(""));
268
+ });
269
+ }
270
+
271
+ function renderEnvBuilderLogin(error = false) {
272
+ return `<!doctype html><html lang="en"><head>
273
+ <meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/>
274
+ <title>HuggingClaw β€” Env Builder</title>
275
+ <style>
276
+ :root{color-scheme:dark;--bg:#08080f;--panel:#12111b;--line:#26243a;--text:#f6f4ff;--muted:#7f7a9e;--bad:#fb7185}
277
+ *{box-sizing:border-box}body{margin:0;min-height:100vh;display:flex;align-items:center;justify-content:center;font-family:Inter,ui-sans-serif,system-ui,-apple-system,sans-serif;background:var(--bg);color:var(--text);padding:24px}
278
+ .card{border:1px solid var(--line);background:var(--panel);border-radius:14px;padding:36px 32px;max-width:400px;width:100%;text-align:center}
279
+ h1{margin:0 0 8px;font-size:1.4rem}
280
+ .sub{color:var(--muted);font-size:.82rem;margin:0 0 24px}
281
+ .row{display:flex;gap:8px;margin-top:16px}
282
+ input{flex:1;background:#0d0c18;border:1px solid var(--line);border-radius:7px;padding:10px 12px;color:var(--text);font-size:.95rem;outline:none}
283
+ input:focus{border-color:#6366f1}
284
+ button{background:#fff;color:#000;border:none;border-radius:7px;padding:10px 20px;font-weight:700;font-size:.95rem;cursor:pointer;transition:opacity .15s}
285
+ button:hover{opacity:.85}
286
+ .err{color:var(--bad);font-size:.82rem;margin-top:10px}
287
+ code{background:#232234;border:1px solid #34324c;border-radius:5px;padding:2px 6px;font-size:.88em}
288
+ </style></head><body>
289
+ <div class="card">
290
+ <h1>βš™οΈ Env Builder</h1>
291
+ <p class="sub">Enter your <code>GATEWAY_TOKEN</code> to continue</p>
292
+ <form method="post" action="/env-builder/login">
293
+ <div class="row">
294
+ <input type="password" name="token" placeholder="GATEWAY_TOKEN" autofocus autocomplete="current-password">
295
+ <button type="submit">Unlock</button>
296
+ </div>
297
+ ${error ? '<p class="err">Invalid token β€” try again</p>' : ""}
298
+ </form>
299
+ </div>
300
+ </body></html>`;
301
+ }
302
+
303
  function badge(label, tone = "neutral") {
304
  return `<span class="badge ${tone}">${escapeHtml(label)}</span>`;
305
  }
 
322
 
323
  const tiles = [
324
  tile({ title: "Gateway", value: badge(data.gatewayReady ? "Online" : "Offline", data.gatewayReady ? "ok" : "off"), detail: `OpenClaw on internal port ${GATEWAY_PORT}`, tone: data.gatewayReady ? "ok" : "off" }),
325
+ tile({ title: "Model", value: `<code>${escapeHtml(LLM_MODEL)}</code>`, detail: LLM_PROVIDER ? `Provider: ${escapeHtml(LLM_PROVIDER)}` : "Primary LLM configured", tone: "neutral" }),
326
  tile({ title: "Runtime", value: escapeHtml(data.uptimeHuman), detail: `Public port ${PORT}`, tone: "neutral" }),
327
+ tile({ title: "Telegram", value: badge(TELEGRAM_ENABLED ? "Enabled" : "Disabled", TELEGRAM_ENABLED ? "ok" : "neutral"), detail: TELEGRAM_ENABLED ? (TELEGRAM_WEBHOOK_URL ? "Webhook" : "Polling") + (process.env.CLOUDFLARE_PROXY_URL ? " via CF proxy" : "") : "Not configured", tone: TELEGRAM_ENABLED ? "ok" : "neutral" }),
328
  ];
329
 
330
 
 
659
  !isSameOriginNav &&
660
  !isFromHFApp;
661
 
662
+ if (pathname === "/env-builder/login") {
663
+ if (req.method === "POST") {
664
+ const body = await readBody(req);
665
+ const token = decodeURIComponent((body.match(/(?:^|&)token=([^&]*)/) || [])[1] || "").replace(/\+/g, " ");
666
+ if (safeEqual(token, GATEWAY_TOKEN)) {
667
+ const cookie = `hc_env_auth=${encodeURIComponent(GATEWAY_TOKEN)}; Path=/env-builder; HttpOnly; SameSite=Strict; Max-Age=86400`;
668
+ res.writeHead(302, { Location: "/env-builder", "Set-Cookie": cookie, "Cache-Control": "no-store" });
669
+ return res.end();
670
+ }
671
+ res.writeHead(200, { "Content-Type": "text/html" });
672
+ return res.end(renderEnvBuilderLogin(true));
673
+ }
674
+ res.writeHead(302, { Location: "/env-builder", "Cache-Control": "no-store" });
675
+ return res.end();
676
+ }
677
+
678
+ if (pathname === "/env-builder/logout") {
679
+ res.writeHead(302, { Location: "/env-builder", "Set-Cookie": "hc_env_auth=; Path=/env-builder; HttpOnly; Max-Age=0", "Cache-Control": "no-store" });
680
+ return res.end();
681
+ }
682
+
683
  if (pathname === "/env-builder" || pathname === "/env-builder/") {
684
  if (isDirectHfSpaceRequest) {
685
  res.writeHead(200, { "Content-Type": "text/html" });
686
  return res.end(renderPrivateRedirect(HF_SPACE_URL));
687
  }
688
+ if (!isEnvBuilderAuthed(req)) {
689
+ res.writeHead(200, { "Content-Type": "text/html" });
690
+ return res.end(renderEnvBuilderLogin(false));
691
+ }
692
  res.writeHead(200, { "Content-Type": "text/html" });
693
  return res.end(renderEnvBuilder());
694
  }
695
 
696
  if (pathname === "/env-builder.js") {
697
+ if (!isEnvBuilderAuthed(req)) {
698
+ res.writeHead(401, { "Content-Type": "text/plain" });
699
+ return res.end("Unauthorized");
700
+ }
701
  try {
702
  const js = fs.readFileSync(require("path").join(__dirname, "env-builder.js"), "utf8");
703
  res.writeHead(200, { "Content-Type": "application/javascript" });
login.html CHANGED
@@ -8,36 +8,26 @@
8
  <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face Logo" style="max-width: 120px; margin-bottom: 24px;">
9
  <h3>HuggingClaw Terminal</h3>
10
  <h4>Welcome to JupyterLab</h4>
11
- <p style="color:#666;">Enter the <strong>JUPYTER_TOKEN</strong> you set in your Space secrets to access the terminal.</p>
12
- <p style="color:#666;">This terminal is mounted at <code>/terminal/</code> inside the same Hugging Face Space as the OpenClaw UI.</p>
13
 
14
  {% if login_available %}
15
- <div class="row" style="display:flex; justify-content:center; margin-top:24px;">
16
- <div class="navbar col-sm-8">
17
- <div class="navbar-inner">
18
- <div class="container">
19
- <div class="center-nav">
20
- <form action="{{base_url}}login?next={{next}}" method="post" class="navbar-form pull-left">
21
- {{ xsrf_form_html() | safe }}
22
- {% if token_available %}
23
- <label for="password_input"><strong>{% trans %}Jupyter token <span title="This is the secret you set up when deploying your JupyterLab terminal">β“˜</span> {% endtrans %}</strong></label>
24
- {% else %}
25
- <label for="password_input"><strong>{% trans %}Jupyter password:{% endtrans %}</strong></label>
26
- {% endif %}
27
- <input type="password" name="password" id="password_input" class="form-control">
28
- <button type="submit" class="btn btn-default" id="login_submit">{% trans %}Log in{% endtrans %}</button>
29
- </form>
30
- </div>
31
- </div>
32
- </div>
33
- </div>
34
  </div>
35
  {% else %}
36
  <p>{% trans %}No login available, you shouldn't be seeing this page.{% endtrans %}</p>
37
  {% endif %}
38
 
39
  <h5 style="margin-top:28px;"><a href="/dashboard">Back to HuggingClaw dashboard</a></h5>
40
- <p>This login page is based on the Hugging Face JupyterLab Space template.</p>
41
 
42
  {% if message %}
43
  <div class="row">
 
8
  <img src="https://huggingface.co/front/assets/huggingface_logo-noborder.svg" alt="Hugging Face Logo" style="max-width: 120px; margin-bottom: 24px;">
9
  <h3>HuggingClaw Terminal</h3>
10
  <h4>Welcome to JupyterLab</h4>
11
+ <p style="color:#666;">Token defaults to your <code>GATEWAY_TOKEN</code>. Set <code>JUPYTER_TOKEN</code> to override.</p>
 
12
 
13
  {% if login_available %}
14
+ <div style="display:flex; justify-content:center; margin-top:24px;">
15
+ <form action="{{base_url}}login?next={{next}}" method="post" style="display:flex; align-items:center; gap:8px;">
16
+ {{ xsrf_form_html() | safe }}
17
+ {% if token_available %}
18
+ <label for="password_input"><strong>{% trans %}Jupyter token <span title="Your GATEWAY_TOKEN (or JUPYTER_TOKEN if set)">β“˜</span>{% endtrans %}</strong></label>
19
+ {% else %}
20
+ <label for="password_input"><strong>{% trans %}Jupyter password:{% endtrans %}</strong></label>
21
+ {% endif %}
22
+ <input type="password" name="password" id="password_input" class="form-control">
23
+ <button type="submit" class="btn btn-default" id="login_submit">{% trans %}Log in{% endtrans %}</button>
24
+ </form>
 
 
 
 
 
 
 
 
25
  </div>
26
  {% else %}
27
  <p>{% trans %}No login available, you shouldn't be seeing this page.{% endtrans %}</p>
28
  {% endif %}
29
 
30
  <h5 style="margin-top:28px;"><a href="/dashboard">Back to HuggingClaw dashboard</a></h5>
 
31
 
32
  {% if message %}
33
  <div class="row">
start.sh CHANGED
@@ -97,7 +97,7 @@ fi
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}")"
@@ -875,12 +875,12 @@ export PATH="$HOME/.local/bin:$PATH"
875
 
876
  # Runtime install fallback: only attempt if DEV_MODE is enabled but install failed during build
877
  if [ "$DEV_MODE_ENABLED" = "true" ] && ! python3 -c "import jupyterlab" >/dev/null 2>&1; then
878
- echo "DEV_MODE enabled but jupyter-lab is missing; attempting runtime install..."
879
- if python3 -m pip install --user --no-cache-dir --break-system-packages "jupyterlab>=4.2,<5" "tornado>=6.3" "ipywidgets>=8.1"; then
880
- echo "Runtime Jupyter install complete."
881
  python3 -c "from pathlib import Path; import shutil, jupyter_server; d=Path(jupyter_server.__file__).parent/'templates'; d.mkdir(parents=True,exist_ok=True); shutil.copyfile('/home/node/app/login.html', d/'login.html')" || true
882
  else
883
- echo "WARNING: Runtime Jupyter install failed; disabling terminal for this boot."
884
  RUNTIME_JUPYTER_ENABLED=false
885
  fi
886
  fi
@@ -896,7 +896,6 @@ if [ -n "${SPACE_HOST:-}" ]; then
896
  else
897
  echo "Routes : /app/ (Control UI)"
898
  fi
899
- echo "Private : open the Hugging Face App tab first; raw https://${SPACE_HOST}/... links can show HF 404 without the embedded Space session."
900
  fi
901
  echo ""
902
 
@@ -984,7 +983,6 @@ start_jupyter_once() {
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.
@@ -1017,7 +1015,7 @@ start_jupyter_once() {
1017
  # Pre-create runtime directory
1018
  mkdir -p "$JUPYTER_ROOT_DIR/.jupyter"
1019
 
1020
- echo "DEV_MODE enabled (${DEV_MODE_RAW}) β€” starting JupyterLab terminal on internal port 8888 (path: /terminal/) with root: $JUPYTER_ROOT_DIR"
1021
  JUPYTER_LOG_FILE="/tmp/jupyterlab.log"
1022
 
1023
  # Use explicit Python to avoid PATH issues; set memory-friendly limits
@@ -1043,7 +1041,7 @@ start_jupyter_once() {
1043
  >> "$JUPYTER_LOG_FILE" 2>&1 &
1044
  JUPYTER_PID=$!
1045
  export JUPYTER_PID
1046
- echo "JupyterLab started (PID: $JUPYTER_PID)"
1047
  }
1048
 
1049
  # BUG FIX #3: DevData restore must happen BEFORE JupyterLab starts.
@@ -1056,9 +1054,9 @@ if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ] && \
1056
  [ -n "${HF_TOKEN:-}" ] && \
1057
  [ -f "/home/node/app/jupyter-devdata-sync.py" ] && \
1058
  [ "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}" != "${BACKUP_DATASET_NAME:-huggingclaw-backup}" ]; then
1059
- echo "DevData : restoring workspace from ${DEVDATA_DATASET_NAME:-huggingclaw-devdata} (before JupyterLab starts)..."
1060
- python3 /home/node/app/jupyter-devdata-sync.py --restore || \
1061
- echo "DevData : restore warning (non-fatal); continuing startup."
1062
  fi
1063
 
1064
  # Fix: reinstall jsonschema AFTER devdata restore β€” restore can overwrite a broken
@@ -1066,9 +1064,7 @@ fi
1066
  # JupyterLab to crash with a circular import error on every boot.
1067
  if [ "$DEV_MODE_ENABLED" = "true" ]; then
1068
  if ! python3 -c "import jsonschema" >/dev/null 2>&1; then
1069
- echo "DevData : jsonschema broken after restore β€” reinstalling (circular import fix)..."
1070
- python3 -m pip install --force-reinstall --no-cache-dir --break-system-packages "jsonschema>=4.0" >/dev/null 2>&1 || true
1071
- echo "DevData : jsonschema reinstall done."
1072
  fi
1073
  fi
1074
 
@@ -1076,8 +1072,6 @@ fi
1076
  # Accessible via /terminal/ path through the health-server proxy
1077
  if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then
1078
  start_jupyter_once
1079
- else
1080
- echo "Jupyter terminal disabled for this boot (DEV_MODE=${DEV_MODE_RAW})."
1081
  fi
1082
 
1083
  if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then
 
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
+ : # auto-enable is silent; 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}")"
 
875
 
876
  # Runtime install fallback: only attempt if DEV_MODE is enabled but install failed during build
877
  if [ "$DEV_MODE_ENABLED" = "true" ] && ! python3 -c "import jupyterlab" >/dev/null 2>&1; then
878
+ echo "Terminal : installing JupyterLab..."
879
+ if python3 -m pip install -q --user --no-cache-dir --break-system-packages "jupyterlab>=4.2,<5" "tornado>=6.3" "ipywidgets>=8.1" >/dev/null 2>&1; then
880
+ echo "Terminal : installed"
881
  python3 -c "from pathlib import Path; import shutil, jupyter_server; d=Path(jupyter_server.__file__).parent/'templates'; d.mkdir(parents=True,exist_ok=True); shutil.copyfile('/home/node/app/login.html', d/'login.html')" || true
882
  else
883
+ echo "Terminal : install failed β€” disabling for this boot"
884
  RUNTIME_JUPYTER_ENABLED=false
885
  fi
886
  fi
 
896
  else
897
  echo "Routes : /app/ (Control UI)"
898
  fi
 
899
  fi
900
  echo ""
901
 
 
983
  # reuse GATEWAY_TOKEN. Both protect the same Space, so the credential is equivalent.
984
  if { [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; } && [ -n "${GATEWAY_TOKEN:-}" ]; then
985
  JUPYTER_TOKEN="$GATEWAY_TOKEN"
 
986
  fi
987
 
988
  # Security guard: refuse to start JupyterLab with the insecure default token.
 
1015
  # Pre-create runtime directory
1016
  mkdir -p "$JUPYTER_ROOT_DIR/.jupyter"
1017
 
1018
+ echo "Terminal : starting (root: $JUPYTER_ROOT_DIR)"
1019
  JUPYTER_LOG_FILE="/tmp/jupyterlab.log"
1020
 
1021
  # Use explicit Python to avoid PATH issues; set memory-friendly limits
 
1041
  >> "$JUPYTER_LOG_FILE" 2>&1 &
1042
  JUPYTER_PID=$!
1043
  export JUPYTER_PID
1044
+ echo "Terminal : started (PID: $JUPYTER_PID)"
1045
  }
1046
 
1047
  # BUG FIX #3: DevData restore must happen BEFORE JupyterLab starts.
 
1054
  [ -n "${HF_TOKEN:-}" ] && \
1055
  [ -f "/home/node/app/jupyter-devdata-sync.py" ] && \
1056
  [ "${DEVDATA_DATASET_NAME:-huggingclaw-devdata}" != "${BACKUP_DATASET_NAME:-huggingclaw-backup}" ]; then
1057
+ echo "DevData : restoring workspace..."
1058
+ python3 /home/node/app/jupyter-devdata-sync.py --restore 2>/dev/null || \
1059
+ echo "DevData : restore warning (non-fatal); continuing startup."
1060
  fi
1061
 
1062
  # Fix: reinstall jsonschema AFTER devdata restore β€” restore can overwrite a broken
 
1064
  # JupyterLab to crash with a circular import error on every boot.
1065
  if [ "$DEV_MODE_ENABLED" = "true" ]; then
1066
  if ! python3 -c "import jsonschema" >/dev/null 2>&1; then
1067
+ python3 -m pip install -q --force-reinstall --no-cache-dir --break-system-packages "jsonschema>=4.0" >/dev/null 2>&1 || true
 
 
1068
  fi
1069
  fi
1070
 
 
1072
  # Accessible via /terminal/ path through the health-server proxy
1073
  if [ "$RUNTIME_JUPYTER_ENABLED" = "true" ]; then
1074
  start_jupyter_once
 
 
1075
  fi
1076
 
1077
  if [ -n "${CLOUDFLARE_WORKERS_TOKEN:-}" ]; then