#!/usr/bin/env node /** * One-time setup for OpenClaw on Hugging Face Spaces. * Runs at container startup; writes or merges openclaw.json from env (Secrets/Variables): * - agents.defaults.model.primary from OPENCLAW_HF_DEFAULT_MODEL (default: DeepSeek-R1). * - gateway.auth: OPENCLAW_GATEWAY_TOKEN (token) or OPENCLAW_GATEWAY_PASSWORD (password); token wins if both set. * - gateway.controlUi.dangerouslyDisableDeviceAuth when auth is set (no device pairing in Spaces). * - gateway.trustedProxies from OPENCLAW_GATEWAY_TRUSTED_PROXIES, or default HF proxy IPs so the UI works without extra config. * - gateway.controlUi.allowedOrigins from OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS (comma-separated origins). * HF_TOKEN is read by the gateway at runtime; this script only writes the above into config. */ import fs from "node:fs"; import path from "node:path"; const home = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user"; const stateDir = path.join(home, ".openclaw"); const configPath = path.join(stateDir, "openclaw.json"); // Token: env var, or read from file if OPENCLAW_GATEWAY_TOKEN_FILE is set (for platforms that mount secrets as files) function readGatewayToken() { const fromEnv = process.env.OPENCLAW_GATEWAY_TOKEN?.trim(); if (fromEnv) return fromEnv; const filePath = process.env.OPENCLAW_GATEWAY_TOKEN_FILE?.trim(); if (filePath && fs.existsSync(filePath)) { try { return fs.readFileSync(filePath, "utf-8").trim(); } catch { return ""; } } return ""; } const defaultModel = process.env.OPENCLAW_HF_DEFAULT_MODEL?.trim() || "huggingface/deepseek-ai/DeepSeek-R1"; const gatewayToken = readGatewayToken(); const gatewayPassword = process.env.OPENCLAW_GATEWAY_PASSWORD?.trim(); // Trusted proxies: from env, or default HF Space proxy IPs so the Control UI works without setting OPENCLAW_GATEWAY_TRUSTED_PROXIES const DEFAULT_HF_TRUSTED_PROXY_IPS = [ "10.16.4.123", "10.16.34.155", "10.20.1.9", "10.20.1.222", "10.20.26.157", "10.20.31.87", ]; const trustedProxiesRaw = process.env.OPENCLAW_GATEWAY_TRUSTED_PROXIES?.trim(); const trustedProxies = trustedProxiesRaw && trustedProxiesRaw.length > 0 ? trustedProxiesRaw.split(",").map((s) => s.trim()).filter(Boolean) : DEFAULT_HF_TRUSTED_PROXY_IPS; // Comma-separated origins allowed for Control UI/WebSocket (e.g. https://your-space.hf.space) const allowedOriginsRaw = process.env.OPENCLAW_CONTROL_UI_ALLOWED_ORIGINS?.trim(); const allowedOrigins = allowedOriginsRaw ? allowedOriginsRaw.split(",").map((s) => s.trim()).filter(Boolean) : []; let config = {}; if (fs.existsSync(configPath)) { try { config = JSON.parse(fs.readFileSync(configPath, "utf-8")); } catch { // keep config empty } } if (!config.agents) config.agents = {}; if (!config.agents.defaults) config.agents.defaults = {}; if (!config.agents.defaults.model) config.agents.defaults.model = {}; config.agents.defaults.model.primary = defaultModel; // Auth: token wins if both set; otherwise password const useTokenAuth = Boolean(gatewayToken); const usePasswordAuth = Boolean(gatewayPassword) && !useTokenAuth; if (useTokenAuth || usePasswordAuth) { if (!config.gateway) config.gateway = {}; if (!config.gateway.auth) config.gateway.auth = {}; if (useTokenAuth) { config.gateway.auth.mode = "token"; config.gateway.auth.token = gatewayToken; } else { config.gateway.auth.mode = "password"; config.gateway.auth.password = gatewayPassword; } } // So Control UI can connect with token/password only (no device pairing); Spaces have no CLI for approve. if (useTokenAuth || usePasswordAuth) { if (!config.gateway) config.gateway = {}; if (!config.gateway.controlUi) config.gateway.controlUi = {}; config.gateway.controlUi.dangerouslyDisableDeviceAuth = true; } // Always set trustedProxies (we have a default for HF so the Control UI works behind the HF proxy) if (!config.gateway) config.gateway = {}; config.gateway.trustedProxies = trustedProxies; if (allowedOrigins.length > 0) { if (!config.gateway) config.gateway = {}; if (!config.gateway.controlUi) config.gateway.controlUi = {}; config.gateway.controlUi.allowedOrigins = allowedOrigins; } fs.mkdirSync(stateDir, { recursive: true }); fs.writeFileSync(configPath, JSON.stringify(config, null, 2), "utf-8"); // Startup log: confirm env was applied (check token_present=1 and auth=token so the UI can connect) const authKind = useTokenAuth ? "token" : usePasswordAuth ? "password" : "none"; const parts = [ `token_present=${useTokenAuth ? "1" : "0"}`, `password_present=${usePasswordAuth ? "1" : "0"}`, `auth=${authKind}`, `trustedProxies=${trustedProxies.length}`, `allowedOrigins=${allowedOrigins.length}`, ]; console.log(`[openclaw-hf-setup] ${parts.join(" ")} -> ${configPath}`); if (authKind === "none") { console.warn( "[openclaw-hf-setup] No auth set. Add OPENCLAW_GATEWAY_TOKEN or OPENCLAW_GATEWAY_PASSWORD in Space Secrets, then restart.", ); }