somratpro Claude Sonnet 4.6 commited on
Commit
775b8b3
Β·
1 Parent(s): f2e8984

feat: add JupyterLab terminal at /terminal/ (DEV_MODE=true)

Browse files

Install JupyterLab in the venv. When DEV_MODE=true and JUPYTER_TOKEN
is set (strong token required β€” full shell access), start JupyterLab
on port 8888 with /terminal/ base URL. health-server proxies HTTP
and WebSocket (required for kernels/terminals) to JupyterLab.

Usage: set DEV_MODE=true and JUPYTER_TOKEN=<openssl rand -hex 32>
in HF Space secrets, then visit /terminal/.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

Files changed (3) hide show
  1. Dockerfile +1 -1
  2. health-server.js +42 -0
  3. start.sh +45 -0
Dockerfile CHANGED
@@ -30,7 +30,7 @@ RUN apt-get update && apt-get install -y --no-install-recommends \
30
  fonts-liberation \
31
  fonts-noto-color-emoji \
32
  && rm -rf /var/lib/apt/lists/* \
33
- && uv pip install --python /opt/hermes/.venv/bin/python --no-cache-dir huggingface_hub hf_transfer
34
 
35
  COPY --chown=hermes:hermes start.sh /opt/huggingmes/start.sh
36
  COPY --chown=hermes:hermes health-server.js /opt/huggingmes/health-server.js
 
30
  fonts-liberation \
31
  fonts-noto-color-emoji \
32
  && rm -rf /var/lib/apt/lists/* \
33
+ && uv pip install --python /opt/hermes/.venv/bin/python --no-cache-dir huggingface_hub hf_transfer jupyterlab
34
 
35
  COPY --chown=hermes:hermes start.sh /opt/huggingmes/start.sh
36
  COPY --chown=hermes:hermes health-server.js /opt/huggingmes/health-server.js
health-server.js CHANGED
@@ -9,7 +9,9 @@ const PORT = Number(process.env.PORT || 7861);
9
  const GATEWAY_PORT = Number(process.env.API_SERVER_PORT || 8642);
10
  const DASHBOARD_PORT = Number(process.env.DASHBOARD_PORT || 9119);
11
  const TELEGRAM_WEBHOOK_PORT = Number(process.env.TELEGRAM_WEBHOOK_PORT || 8765);
 
12
  const GATEWAY_HOST = "127.0.0.1";
 
13
  const startTime = Date.now();
14
  const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
15
  const APP_BASE = "/app";
@@ -662,10 +664,50 @@ const server = http.createServer(async (req, res) => {
662
  return;
663
  }
664
 
 
 
 
 
 
 
 
 
 
 
 
 
 
665
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
666
  res.end("Not found");
667
  });
668
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
669
  server.listen(PORT, "0.0.0.0", () => {
670
  console.log(`HuggingMes dashboard listening on 0.0.0.0:${PORT}`);
671
  });
 
9
  const GATEWAY_PORT = Number(process.env.API_SERVER_PORT || 8642);
10
  const DASHBOARD_PORT = Number(process.env.DASHBOARD_PORT || 9119);
11
  const TELEGRAM_WEBHOOK_PORT = Number(process.env.TELEGRAM_WEBHOOK_PORT || 8765);
12
+ const JUPYTER_PORT = 8888;
13
  const GATEWAY_HOST = "127.0.0.1";
14
+ const TERMINAL_BASE = "/terminal";
15
  const startTime = Date.now();
16
  const API_SERVER_KEY = process.env.API_SERVER_KEY || "";
17
  const APP_BASE = "/app";
 
664
  return;
665
  }
666
 
667
+ if (path === TERMINAL_BASE || path.startsWith(`${TERMINAL_BASE}/`)) {
668
+ if (!requireAuth(req, res)) return;
669
+ canConnect(JUPYTER_PORT).then((up) => {
670
+ if (!up) {
671
+ res.writeHead(503, { "content-type": "text/plain; charset=utf-8" });
672
+ res.end("JupyterLab is not running. Set DEV_MODE=true and JUPYTER_TOKEN in Space secrets to enable /terminal/.");
673
+ return;
674
+ }
675
+ proxyRequest(req, res, JUPYTER_PORT);
676
+ });
677
+ return;
678
+ }
679
+
680
  res.writeHead(404, { "content-type": "text/plain; charset=utf-8" });
681
  res.end("Not found");
682
  });
683
 
684
+ // ── WebSocket upgrade (JupyterLab terminals + kernels need this) ──
685
+ server.on("upgrade", (req, socket, head) => {
686
+ const { pathname } = new URL(req.url, "http://localhost");
687
+ const isJupyter = pathname === TERMINAL_BASE || pathname.startsWith(`${TERMINAL_BASE}/`);
688
+ const targetPort = isJupyter ? JUPYTER_PORT : GATEWAY_PORT;
689
+ const ps = net.createConnection(targetPort, GATEWAY_HOST, () => {
690
+ ps.write(`${req.method} ${req.url} HTTP/${req.httpVersion}\r\n`);
691
+ ps.write(`Host: ${GATEWAY_HOST}:${targetPort}\r\n`);
692
+ ps.write(`X-Forwarded-Host: ${req.headers.host || ""}\r\n`);
693
+ ps.write("X-Forwarded-Proto: https\r\n");
694
+ for (let i = 0; i < req.rawHeaders.length; i += 2) {
695
+ const lower = req.rawHeaders[i].toLowerCase();
696
+ if (["host", "x-forwarded-host", "x-forwarded-proto"].includes(lower)) continue;
697
+ ps.write(`${req.rawHeaders[i]}: ${req.rawHeaders[i + 1]}\r\n`);
698
+ }
699
+ ps.write("\r\n");
700
+ if (head && head.length) ps.write(head);
701
+ ps.pipe(socket).pipe(ps);
702
+ });
703
+ ps.on("error", () => socket.destroy());
704
+ ps.on("close", () => socket.destroy());
705
+ socket.on("error", () => ps.destroy());
706
+ socket.on("close", () => ps.destroy());
707
+ });
708
+
709
+ server.timeout = 0;
710
+ server.keepAliveTimeout = 65000;
711
  server.listen(PORT, "0.0.0.0", () => {
712
  console.log(`HuggingMes dashboard listening on 0.0.0.0:${PORT}`);
713
  });
start.sh CHANGED
@@ -308,6 +308,49 @@ echo "Dashboard : http://127.0.0.1:${DASHBOARD_PORT}"
308
  echo "Gateway : http://127.0.0.1:${GATEWAY_API_PORT}"
309
  echo ""
310
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
  # ── Trap SIGTERM for graceful shutdown ──
312
  graceful_shutdown() {
313
  echo "Shutting down HuggingMes..."
@@ -371,6 +414,8 @@ if [ -n "${HF_TOKEN:-}" ]; then
371
  python3 -u "$APP_DIR/hermes-sync.py" loop &
372
  fi
373
 
 
 
374
  wait "$GATEWAY_PID"
375
 
376
  # Gateway exited (e.g. user restarted from Hermes UI). Sync before container dies.
 
308
  echo "Gateway : http://127.0.0.1:${GATEWAY_API_PORT}"
309
  echo ""
310
 
311
+ # ── JupyterLab terminal (DEV_MODE=true + JUPYTER_TOKEN required) ──
312
+ start_jupyter() {
313
+ if [ "${DEV_MODE:-false}" != "true" ]; then
314
+ echo "JupyterLab disabled (set DEV_MODE=true to enable /terminal/)."
315
+ return 0
316
+ fi
317
+ if [ -z "${JUPYTER_TOKEN:-}" ] || [ "${JUPYTER_TOKEN}" = "huggingface" ]; then
318
+ echo "ERROR: JUPYTER_TOKEN unset or insecure default. JupyterLab grants full shell β€” set a strong token." >&2
319
+ echo " Hint: openssl rand -hex 32" >&2
320
+ return 1
321
+ fi
322
+ if ! python3 -c "import jupyterlab" >/dev/null 2>&1; then
323
+ echo "WARNING: jupyterlab not installed; skipping terminal." >&2
324
+ return 1
325
+ fi
326
+ local root_dir="${JUPYTER_ROOT_DIR:-$HERMES_HOME/workspace}"
327
+ mkdir -p "$root_dir"
328
+ ln -sfn "$HERMES_HOME" "$root_dir/HuggingMes" 2>/dev/null || true
329
+ echo "Starting JupyterLab terminal on port 8888 (path: /terminal/) root: $root_dir"
330
+ python3 -m jupyterlab \
331
+ --ip 127.0.0.1 \
332
+ --port 8888 \
333
+ --no-browser \
334
+ --IdentityProvider.token="$JUPYTER_TOKEN" \
335
+ --ServerApp.base_url=/terminal/ \
336
+ --ServerApp.terminals_enabled=True \
337
+ --ServerApp.terminado_settings='{"shell_command":["/bin/bash","-i"]}' \
338
+ --ServerApp.allow_origin='*' \
339
+ --ServerApp.allow_remote_access=True \
340
+ --ServerApp.trust_xheaders=True \
341
+ --ServerApp.tornado_settings="{'headers': {'Content-Security-Policy': 'frame-ancestors *'}}" \
342
+ --IdentityProvider.cookie_options="{'SameSite': 'None', 'Secure': True}" \
343
+ --ServerApp.disable_check_xsrf=True \
344
+ --LabApp.news_url=None \
345
+ --LabApp.check_for_updates_class=jupyterlab.NeverCheckForUpdate \
346
+ --ServerApp.log_level=WARN \
347
+ --ServerApp.root_dir="$root_dir" \
348
+ >> "$HERMES_HOME/logs/jupyter.log" 2>&1 &
349
+ JUPYTER_PID=$!
350
+ export JUPYTER_PID
351
+ echo "JupyterLab started (PID: $JUPYTER_PID)"
352
+ }
353
+
354
  # ── Trap SIGTERM for graceful shutdown ──
355
  graceful_shutdown() {
356
  echo "Shutting down HuggingMes..."
 
414
  python3 -u "$APP_DIR/hermes-sync.py" loop &
415
  fi
416
 
417
+ start_jupyter
418
+
419
  wait "$GATEWAY_PID"
420
 
421
  # Gateway exited (e.g. user restarted from Hermes UI). Sync before container dies.