Spaces:
Runtime error
Runtime error
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 +2 -2
- README.md +68 -39
- openclaw.json +1 -1
- patches/hf-spaces-allow-iframe-embedding.patch +1 -2
- patches/hf-spaces-global-token-fallback.patch +0 -37
- scripts/entrypoint.sh +0 -7
- scripts/inject-token.sh +0 -32
- scripts/sync_hf.py +9 -4
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
|
| 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
|
|
|
|
| 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:
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: false
|
| 8 |
license: mit
|
| 9 |
-
short_description:
|
| 10 |
app_port: 7860
|
| 11 |
---
|
| 12 |
|
| 13 |
-
#
|
| 14 |
|
| 15 |
-
|
| 16 |
|
| 17 |
-
|
| 18 |
-
git clone https://huggingface.co/spaces/tao-shen/HuggingClaw
|
| 19 |
-
cd HuggingClaw
|
| 20 |
-
```
|
| 21 |
|
| 22 |
-
##
|
| 23 |
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
|
|
|
| 28 |
|
| 29 |
-
##
|
| 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 |
-
##
|
| 46 |
|
| 47 |
-
|
| 48 |
-
-
|
| 49 |
-
-
|
| 50 |
|
| 51 |
-
##
|
| 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 |
-
|
| 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": { "
|
| 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 |
-
|
| 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 |
-
#
|
| 325 |
-
|
|
|
|
|
|
|
| 326 |
data["gateway"] = {
|
| 327 |
"mode": "local",
|
| 328 |
"bind": "lan",
|
| 329 |
"port": 7860,
|
| 330 |
-
"auth":
|
| 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=
|
| 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", {})
|