Spaces:
Running
Running
Merge branch 'main' into edit
Browse files- .dockerignore +12 -0
- Dockerfile +5 -3
- README.md +5 -4
- env-builder.js +8 -9
- health-server.js +96 -7
- login.html +12 -22
- 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 |
-
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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` (
|
| 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
|
| 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": "
|
| 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 |
-
"
|
| 857 |
-
"
|
| 858 |
-
"
|
| 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 |
-
//
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
process.env.HUGGINGCLAW_JUPYTER_ENABLED ||
|
| 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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
| 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 ? "
|
| 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,"&").replace(/</g,"<").replace(/>/g,">").replace(/"/g,""");
|
| 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;">
|
| 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
|
| 16 |
-
<
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 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 |
-
|
| 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 "
|
| 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 "
|
| 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 "
|
| 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 "
|
| 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 "
|
| 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
|
| 1060 |
-
python3 /home/node/app/jupyter-devdata-sync.py --restore || \
|
| 1061 |
-
echo "DevData
|
| 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 |
-
|
| 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
|