tao-shen Claude Opus 4.6 commited on
Commit
201eb72
·
1 Parent(s): ce71ed6

feat: switch to password auth + project README

Browse files

- Auth: password mode (default: "huggingclaw", override via OPENCLAW_PASSWORD secret)
- Removed: inject-token.sh, global-token-fallback patch (no longer needed)
- CSP: reverted script-src to 'self' (no inline scripts)
- README: project intro, quick start, architecture, security docs

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

Dockerfile CHANGED
@@ -52,10 +52,10 @@ RUN echo "[build][layer2] Clone + install + build..." && START=$(date +%s) \
52
  && echo "[build] version: $(cat /app/openclaw/.version)" \
53
  && echo "[build][layer2] Total clone+install+build: $(($(date +%s) - START))s"
54
 
55
- # ── Layer 3 (node): Scripts + Config + Token 注入 ─────────────────────────────
56
  COPY --chown=node:node scripts /home/node/scripts
57
  COPY --chown=node:node openclaw.json /home/node/scripts/openclaw.json.default
58
- RUN chmod +x /home/node/scripts/entrypoint.sh /home/node/scripts/sync_hf.py /home/node/scripts/inject-token.sh
59
 
60
  ENV NODE_ENV=production
61
  ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/openclaw/empty-bundled-plugins
 
52
  && echo "[build] version: $(cat /app/openclaw/.version)" \
53
  && echo "[build][layer2] Total clone+install+build: $(($(date +%s) - START))s"
54
 
55
+ # ── Layer 3 (node): Scripts + Config ──────────────────────────────────────────
56
  COPY --chown=node:node scripts /home/node/scripts
57
  COPY --chown=node:node openclaw.json /home/node/scripts/openclaw.json.default
58
+ RUN chmod +x /home/node/scripts/entrypoint.sh /home/node/scripts/sync_hf.py
59
 
60
  ENV NODE_ENV=production
61
  ENV OPENCLAW_BUNDLED_PLUGINS_DIR=/app/openclaw/empty-bundled-plugins
README.md CHANGED
@@ -1,58 +1,87 @@
1
  ---
2
  title: HuggingClaw
3
- emoji: 🔥
4
- colorFrom: gray
5
- colorTo: yellow
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
- short_description: HuggingClaw
10
  app_port: 7860
11
  ---
12
 
13
- ## 初始化与运行
14
 
15
- ### 克隆仓库
16
 
17
- ```bash
18
- git clone https://huggingface.co/spaces/tao-shen/HuggingClaw
19
- cd HuggingClaw
20
- ```
21
 
22
- ### Hugging Face Space 上运行
23
 
24
- 1. Fork 或使用本 Space,在 **Settings Repository secrets** 中配置:
25
- - `HF_TOKEN`:具有写权限的 HF Access Token
26
- - `OPENCLAW_DATASET_REPO`:用于持久化的 Dataset 仓库(如 `username/openclaw-backup`)
27
- 2. 重新启动 Space 即可。
 
28
 
29
- ### 本地 Docker 运行(可选)
30
 
31
- 1. 复制环境变量模板并填写必填项:
32
- ```bash
33
- cp .env.example .env
34
- # 编辑 .env,至少填写 HF_TOKEN 和 OPENCLAW_DATASET_REPO
35
- ```
36
- 2. 构建并运行(需先安装 Docker):
37
- ```bash
38
- docker build -t huggingclaw .
39
- docker run --rm -p 7860:7860 --env-file .env huggingclaw
40
- ```
41
- 3. 浏览器访问 `http://localhost:7860`。
42
 
43
- ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
44
 
45
- ## Environment Variables
46
 
47
- ### Persistence (Required)
48
- - `HF_TOKEN` - Hugging Face access token with write permissions
49
- - `OPENCLAW_DATASET_REPO` - Dataset repository for backup (e.g., `username/dataset-name`)
50
 
51
- ### Telegram Bot (Optional)
52
- - `TELEGRAM_BOT_TOKEN` - Your Telegram bot token
53
- - `TELEGRAM_BOT_NAME` - Bot username
54
- - `TELEGRAM_ALLOW_USER` - Your Telegram username to allow
55
 
56
- ### Optional
57
- - `SYNC_INTERVAL` - Seconds between syncs (default: 120)
58
- - `ENABLE_AUX_SERVICES` - Enable aux services (default: false)
 
1
  ---
2
  title: HuggingClaw
3
+ emoji: 🐾
4
+ colorFrom: purple
5
+ colorTo: blue
6
  sdk: docker
7
  pinned: false
8
  license: mit
9
+ short_description: Deploy OpenClaw on HuggingFace Spaces
10
  app_port: 7860
11
  ---
12
 
13
+ # HuggingClaw
14
 
15
+ **Deploy [OpenClaw](https://github.com/openclaw/openclaw) on HuggingFace Spaces** — no hardware required.
16
 
17
+ OpenClaw is a self-hosted AI assistant platform with Telegram, WhatsApp integration and a web-based Control UI. HuggingClaw packages it for one-click cloud deployment on HuggingFace Spaces.
 
 
 
18
 
19
+ ## Why HuggingFace Spaces?
20
 
21
+ - **Zero hardware** runs on HF's free CPU tier
22
+ - **Always online** no need to keep your computer running
23
+ - **Persistent storage** auto-syncs data to a HF Dataset repo
24
+ - **HTTPS built-in** — secure WebSocket connections out of the box
25
+ - **One-click deploy** — fork this Space, set secrets, done
26
 
27
+ ## Quick Start
28
 
29
+ ### 1. Fork this Space
 
 
 
 
 
 
 
 
 
 
30
 
31
+ Click **Duplicate this Space** on HuggingFace.
32
+
33
+ ### 2. Set Secrets
34
+
35
+ Go to **Settings > Repository secrets** and add:
36
+
37
+ | Secret | Required | Description |
38
+ |--------|:--------:|-------------|
39
+ | `OPENCLAW_PASSWORD` | Recommended | Password for the Control UI (default: `huggingclaw`) |
40
+ | `HF_TOKEN` | Yes | HF Access Token with write permission (for data persistence) |
41
+ | `OPENCLAW_DATASET_REPO` | Yes | Dataset repo for backup, e.g. `your-name/openclaw-data` |
42
+ | `OPENROUTER_API_KEY` | No | [OpenRouter](https://openrouter.ai) API key for LLM access |
43
+ | `TELEGRAM_BOT_TOKEN` | No | Telegram bot token from [@BotFather](https://t.me/BotFather) |
44
+ | `TELEGRAM_BOT_NAME` | No | Telegram bot username |
45
+ | `TELEGRAM_ALLOW_USER` | No | Telegram username allowed to chat |
46
+
47
+ ### 3. Open the Control UI
48
+
49
+ Visit your Space URL. Enter the password in the settings panel to connect.
50
+
51
+ ## Architecture
52
+
53
+ ```
54
+ HuggingFace Space (Docker)
55
+ ├── OpenClaw (Node.js) — AI assistant engine
56
+ │ ├── Control UI — Web dashboard (port 7860)
57
+ │ ├── Telegram extension — Bot integration
58
+ │ └── WhatsApp extension — Messaging integration
59
+ ├── sync_hf.py — Auto-sync ~/.openclaw ↔ HF Dataset
60
+ ├── dns-resolve.py — DNS pre-resolution for WhatsApp
61
+ └── entrypoint.sh — Startup orchestration
62
+ ```
63
+
64
+ ## Local Development
65
+
66
+ ```bash
67
+ git clone https://huggingface.co/spaces/tao-shen/HuggingClaw
68
+ cd HuggingClaw
69
+ docker build -t huggingclaw .
70
+ docker run --rm -p 7860:7860 \
71
+ -e OPENCLAW_PASSWORD=your-password \
72
+ -e HF_TOKEN=hf_xxx \
73
+ -e OPENCLAW_DATASET_REPO=your-name/openclaw-data \
74
+ huggingclaw
75
+ ```
76
+
77
+ Open `http://localhost:7860` in your browser.
78
 
79
+ ## Security
80
 
81
+ - **Password-protected** — the Control UI requires a password to connect
82
+ - **Secrets stay server-side** API keys are never exposed to the browser
83
+ - **CSP headers** Content Security Policy restricts script/resource loading
84
 
85
+ ## License
 
 
 
86
 
87
+ MIT
 
 
openclaw.json CHANGED
@@ -3,7 +3,7 @@
3
  "mode": "local",
4
  "bind": "lan",
5
  "port": 7860,
6
- "auth": { "token": "hf-space-public-token" },
7
  "trustedProxies": [
8
  "0.0.0.0/0"
9
  ],
 
3
  "mode": "local",
4
  "bind": "lan",
5
  "port": 7860,
6
+ "auth": { "password": "__OPENCLAW_PASSWORD__" },
7
  "trustedProxies": [
8
  "0.0.0.0/0"
9
  ],
patches/hf-spaces-allow-iframe-embedding.patch CHANGED
@@ -7,9 +7,8 @@ index 8a7b56f..62b0dfd 100644
7
  "base-uri 'none'",
8
  "object-src 'none'",
9
  - "frame-ancestors 'none'",
10
- - "script-src 'self'",
11
  + "frame-ancestors 'self' https://huggingface.co https://*.hf.space",
12
- + "script-src 'self' 'unsafe-inline'",
13
  "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
14
  "img-src 'self' data: https:",
15
  "font-src 'self' https://fonts.gstatic.com",
 
7
  "base-uri 'none'",
8
  "object-src 'none'",
9
  - "frame-ancestors 'none'",
 
10
  + "frame-ancestors 'self' https://huggingface.co https://*.hf.space",
11
+ "script-src 'self'",
12
  "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com",
13
  "img-src 'self' data: https:",
14
  "font-src 'self' https://fonts.gstatic.com",
patches/hf-spaces-global-token-fallback.patch DELETED
@@ -1,37 +0,0 @@
1
- diff --git a/ui/src/ui/storage.ts b/ui/src/ui/storage.ts
2
- index b32e6c3..763a543 100644
3
- --- a/ui/src/ui/storage.ts
4
- +++ b/ui/src/ui/storage.ts
5
- @@ -17,6 +17,14 @@ export type UiSettings = {
6
- locale?: string;
7
- };
8
-
9
- +// Read an injected auth token from a global variable.
10
- +// Used by HF Spaces where localStorage may be unavailable
11
- +// (e.g. third-party iframe in incognito mode).
12
- +function getInjectedToken(): string {
13
- + const w = globalThis as Record<string, unknown>;
14
- + return typeof w.__OPENCLAW_AUTH_TOKEN__ === "string" ? (w.__OPENCLAW_AUTH_TOKEN__ as string) : "";
15
- +}
16
- +
17
- export function loadSettings(): UiSettings {
18
- const defaultUrl = (() => {
19
- const proto = location.protocol === "https:" ? "wss" : "ws";
20
- @@ -25,7 +33,7 @@ export function loadSettings(): UiSettings {
21
-
22
- const defaults: UiSettings = {
23
- gatewayUrl: defaultUrl,
24
- - token: "",
25
- + token: getInjectedToken(),
26
- sessionKey: "main",
27
- lastActiveSessionKey: "main",
28
- theme: "system",
29
- @@ -47,7 +55,7 @@ export function loadSettings(): UiSettings {
30
- typeof parsed.gatewayUrl === "string" && parsed.gatewayUrl.trim()
31
- ? parsed.gatewayUrl.trim()
32
- : defaults.gatewayUrl,
33
- - token: typeof parsed.token === "string" ? parsed.token : defaults.token,
34
- + token: typeof parsed.token === "string" && parsed.token ? parsed.token : defaults.token,
35
- sessionKey:
36
- typeof parsed.sessionKey === "string" && parsed.sessionKey.trim()
37
- ? parsed.sessionKey.trim()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/entrypoint.sh CHANGED
@@ -48,13 +48,6 @@ touch /home/node/logs/app.log
48
  ENTRYPOINT_END=$(date +%s)
49
  echo "[TIMER] Entrypoint (before sync_hf.py): $((ENTRYPOINT_END - BOOT_START))s"
50
 
51
- # ── Inject auth token into Control UI HTML ─────────────────────────────────
52
- INJECT_START=$(date +%s)
53
- if [ -x /home/node/scripts/inject-token.sh ]; then
54
- bash /home/node/scripts/inject-token.sh
55
- fi
56
- echo "[TIMER] Token inject: $(($(date +%s) - INJECT_START))s"
57
-
58
  # ── Set version from build artifact ────────────────────────────────────────
59
  if [ -f /app/openclaw/.version ]; then
60
  export OPENCLAW_VERSION=$(cat /app/openclaw/.version)
 
48
  ENTRYPOINT_END=$(date +%s)
49
  echo "[TIMER] Entrypoint (before sync_hf.py): $((ENTRYPOINT_END - BOOT_START))s"
50
 
 
 
 
 
 
 
 
51
  # ── Set version from build artifact ────────────────────────────────────────
52
  if [ -f /app/openclaw/.version ]; then
53
  export OPENCLAW_VERSION=$(cat /app/openclaw/.version)
scripts/inject-token.sh DELETED
@@ -1,32 +0,0 @@
1
- #!/bin/sh
2
- # Inject auto-token config into Control UI so the browser auto-connects
3
- # The token must match gateway.auth.token in openclaw.json
4
- TOKEN="hf-space-public-token"
5
-
6
- INDEX_HTML="/app/openclaw/dist/control-ui/index.html"
7
-
8
- if [ ! -f "$INDEX_HTML" ]; then
9
- echo "[inject-token] WARNING: $INDEX_HTML not found, skipping"
10
- exit 0
11
- fi
12
-
13
- # Create the injection script
14
- # 1. Set window.__OPENCLAW_AUTH_TOKEN__ — always works (even when localStorage is blocked in iframe/incognito)
15
- # 2. Also try localStorage as a fallback for the original UI code path
16
- INJECT_SCRIPT="<script>window.__OPENCLAW_AUTH_TOKEN__='${TOKEN}';try{var K='openclaw.control.settings.v1',s=JSON.parse(localStorage.getItem(K)||'{}');s.token='${TOKEN}';localStorage.setItem(K,JSON.stringify(s))}catch(e){}</script>"
17
-
18
- # Use python3 for reliable string replacement (avoids sed delimiter issues)
19
- python3 -c "
20
- import sys
21
- f = '${INDEX_HTML}'
22
- with open(f, 'r') as fh:
23
- html = fh.read()
24
- inject = '''${INJECT_SCRIPT}'''
25
- if '</head>' in html and '__OPENCLAW_AUTH_TOKEN__' not in html:
26
- html = html.replace('</head>', inject + '</head>')
27
- with open(f, 'w') as fh:
28
- fh.write(html)
29
- print('[inject-token] Token injected into ' + f)
30
- else:
31
- print('[inject-token] Skipped (already injected or no </head> found)')
32
- "
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
scripts/sync_hf.py CHANGED
@@ -64,6 +64,9 @@ TELEGRAM_ALLOW_USER = os.environ.get("TELEGRAM_ALLOW_USER", "")
64
  # OpenRouter API key for free models (must be set via environment variable)
65
  OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
66
 
 
 
 
67
  SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL", "120"))
68
 
69
  # Setup logging
@@ -321,13 +324,15 @@ class OpenClawFullSync:
321
  data["plugins"]["locations"] = [l for l in locs if l != "/dev/null"]
322
 
323
  # Force full gateway config for HF Spaces
324
- # Note: Dockerfile injects "openclaw-space-default" token into Control UI,
325
- # so we MUST set it here to match what the browser sends.
 
 
326
  data["gateway"] = {
327
  "mode": "local",
328
  "bind": "lan",
329
  "port": 7860,
330
- "auth": {"token": "hf-space-public-token"},
331
  "trustedProxies": ["0.0.0.0/0"],
332
  "controlUi": {
333
  "allowInsecureAuth": True,
@@ -339,7 +344,7 @@ class OpenClawFullSync:
339
  ]
340
  }
341
  }
342
- print("[SYNC] Set gateway config (auth=default, trustedProxies=all)")
343
 
344
  # Ensure agents defaults
345
  data.setdefault("agents", {}).setdefault("defaults", {}).setdefault("model", {})
 
64
  # OpenRouter API key for free models (must be set via environment variable)
65
  OPENROUTER_API_KEY = os.environ.get("OPENROUTER_API_KEY", "")
66
 
67
+ # Gateway password (override via HF Secret OPENCLAW_PASSWORD)
68
+ OPENCLAW_PASSWORD = os.environ.get("OPENCLAW_PASSWORD", "huggingclaw")
69
+
70
  SYNC_INTERVAL = int(os.environ.get("SYNC_INTERVAL", "120"))
71
 
72
  # Setup logging
 
324
  data["plugins"]["locations"] = [l for l in locs if l != "/dev/null"]
325
 
326
  # Force full gateway config for HF Spaces
327
+ # Password auth: user must enter password in Control UI settings
328
+ if not OPENCLAW_PASSWORD:
329
+ print("[SYNC] WARNING: OPENCLAW_PASSWORD not set! Gateway will auto-generate a random token.")
330
+ auth = {"password": OPENCLAW_PASSWORD} if OPENCLAW_PASSWORD else {}
331
  data["gateway"] = {
332
  "mode": "local",
333
  "bind": "lan",
334
  "port": 7860,
335
+ "auth": auth,
336
  "trustedProxies": ["0.0.0.0/0"],
337
  "controlUi": {
338
  "allowInsecureAuth": True,
 
344
  ]
345
  }
346
  }
347
+ print(f"[SYNC] Set gateway config (auth={'password' if OPENCLAW_PASSWORD else 'auto-generated'}, trustedProxies=all)")
348
 
349
  # Ensure agents defaults
350
  data.setdefault("agents", {}).setdefault("defaults", {}).setdefault("model", {})