import fs from "node:fs"; import path from "node:path"; import https from "node:https"; // ============================================================ // OpenClaw HF Spaces - Production Config Writer // // Storage: HF Storage Bucket at /data (100 GB persistent). // All runtime data lives in OPENCLAW_HOME/.openclaw/. // // Config priority: // 1. Existing openclaw.json in bucket -> PATCH env vars only // 2. /app/openclaw-template.json -> use as base + patch // 3. Neither -> minimal fresh + patch // // Always patched from env vars (never lost on restart): // gateway.auth, gateway.controlUi, gateway.trustedProxies // agents.defaults.workspace, agents.defaults.model // env.vars, models.providers.*.apiKey, channels.telegram // ============================================================ var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user"; var STATE_DIR = path.join(HOME, ".openclaw"); var CONFIG_PATH = path.join(STATE_DIR, "openclaw.json"); var WORKSPACE = path.join(STATE_DIR, "workspace"); var SPACE_HOST = (process.env.SPACE_HOST || "").trim(); var TEMPLATE = "/app/openclaw-template.json"; function log(msg) { console.log("[setup] " + msg); } function err(msg) { console.error("[setup] ERROR: " + msg); } log("Starting... HOME=" + HOME); // ---------------------------------------------------------------- // Helpers // ---------------------------------------------------------------- function envStr(key) { return (process.env[key] || "").trim(); } function parseList(val) { if (!val || !val.trim()) return []; return val.split(",").map(function(s){ return s.trim(); }).filter(Boolean); } // ---------------------------------------------------------------- // Auth (required) // ---------------------------------------------------------------- var gatewayToken = envStr("OPENCLAW_GATEWAY_TOKEN"); var gatewayPassword = envStr("OPENCLAW_GATEWAY_PASSWORD"); if (!gatewayToken && !gatewayPassword) { err("FATAL: set OPENCLAW_GATEWAY_TOKEN in Secrets"); process.exit(0); } // ---------------------------------------------------------------- // Default model // ---------------------------------------------------------------- var defaultModel = envStr("OPENCLAW_HF_DEFAULT_MODEL") || "google/gemini-2.0-flash"; // ---------------------------------------------------------------- // Provider API key detection // ---------------------------------------------------------------- var EXCLUDE_PREFIXES = [ "OPENCLAW_", "SPACE_", "SYSTEM_", "HF_", "NODE_", "PATH", "HOME", "USER", "PWD", "LANG", "LC_", "npm_", "HOSTNAME", "SHELL", "TERM", "SHLVL" ]; var INCLUDE_SUFFIXES = [ "_API_KEY", "_SECRET_KEY", "_ACCESS_TOKEN", "_BOT_TOKEN", "_AUTH_TOKEN", "_APP_KEY" ]; function isProviderKey(k) { var i; for (i = 0; i < EXCLUDE_PREFIXES.length; i++) { if (k.indexOf(EXCLUDE_PREFIXES[i]) === 0) return false; } for (i = 0; i < INCLUDE_SUFFIXES.length; i++) { var s = INCLUDE_SUFFIXES[i]; if (k.length > s.length && k.indexOf(s) === k.length - s.length) return true; } return false; } var providerKeys = Object.keys(process.env).filter(function(k) { return isProviderKey(k) && envStr(k); }).sort(); log("Provider keys (" + providerKeys.length + "): " + providerKeys.join(", ")); // ---------------------------------------------------------------- // Trusted proxies (HF internal + standard RFC1918) // ---------------------------------------------------------------- var envProxies = parseList(envStr("OPENCLAW_GATEWAY_TRUSTED_PROXIES")); var trustedProxies = envProxies.length > 0 ? envProxies : [ "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16", "10.16.0.0/12", "10.20.0.0/12", "10.16.4.123", "10.16.7.92", "10.16.18.232", "10.16.34.155", "10.16.43.133", "10.16.1.206", "10.16.37.110", "10.16.43.246", "10.20.1.9", "10.20.1.222", "10.20.26.157", "10.20.31.87", "10.20.0.1", "172.17.0.1", "127.0.0.1" ]; // ---------------------------------------------------------------- // Provider -> env var mapping // ---------------------------------------------------------------- var PROVIDER_KEY_MAP = { "openrouter": "OPENROUTER_API_KEY", "siliconflow": "SILICONFLOW_API_KEY", "deepseek": "DEEPSEEK_API_KEY", "groq": "GROQ_API_KEY", "google": "GOOGLE_API_KEY", "gemini": "GEMINI_API_KEY", "minimax": "MINIMAX_API_KEY", "zai": "ZAI_API_KEY", "github": "GITHUB_MODELS_API_KEY", "github-copilot": "GITHUB_MODELS_API_KEY", "openai": "OPENAI_API_KEY", "anthropic": "ANTHROPIC_API_KEY" }; function patchProviderKeys(config) { if (!config.models || !config.models.providers) return; var providers = config.models.providers; Object.keys(providers).forEach(function(name) { var lower = name.toLowerCase(); var envKey = PROVIDER_KEY_MAP[lower]; var val = envKey ? envStr(envKey) : ""; if (!val) { // fuzzy match: find any provider key whose name contains the provider name var found = providerKeys.find(function(k) { return k.toLowerCase().indexOf(lower) >= 0; }); if (found) val = envStr(found); } if (val) { providers[name].apiKey = val; log("Provider key set: " + name + " <- " + (envKey || "fuzzy")); } else if (providers[name].apiKey === "WILL_BE_SET_BY_ENV") { delete providers[name].apiKey; log("Provider key missing (skipped): " + name); } }); } // ---------------------------------------------------------------- // Telegram webhook helpers // ---------------------------------------------------------------- function tgRequest(token, method, body) { return new Promise(function(resolve) { var data = JSON.stringify(body || {}); var req = https.request({ hostname: "api.telegram.org", path: "/bot" + token + "/" + method, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(data) } }, function(res) { var buf = ""; res.on("data", function(c) { buf += c; }); res.on("end", function() { try { resolve(JSON.parse(buf)); } catch(e) { resolve(null); } }); }); req.on("error", function() { resolve(null); }); req.setTimeout(10000, function() { req.destroy(); resolve(null); }); req.write(data); req.end(); }); } async function setupWebhook(token) { if (!SPACE_HOST) { log("Telegram: no SPACE_HOST - webhook skipped"); return; } var targetUrl = "https://" + SPACE_HOST + "/tg-webhook"; // Check if webhook already points to our URL - skip if already set var info = await tgRequest(token, "getWebhookInfo", {}); if (info && info.ok && info.result && info.result.url === targetUrl) { log("Telegram: webhook already set -> " + targetUrl); return; } var r = await tgRequest(token, "setWebhook", { url: targetUrl, drop_pending_updates: true, max_connections: 10 }); if (r && r.ok) { log("Telegram: webhook registered -> " + targetUrl); } else { var desc = r && r.description ? r.description : "unknown error"; log("Telegram: webhook failed (" + desc + ")"); log("Telegram: set manually at:"); log(" https://api.telegram.org/bot" + token + "/setWebhook?url=" + targetUrl + "&drop_pending_updates=true"); } } // ---------------------------------------------------------------- // Seed workspace (first boot only - bucket persists after) // ---------------------------------------------------------------- function seedWorkspace() { var soul = path.join(WORKSPACE, "SOUL.md"); if (!fs.existsSync(soul)) { fs.writeFileSync(soul, [ "# Soul", "", "You are a helpful, warm, concise AI assistant.", "", "## Language", "", "Default language: Simplified Chinese.", "Always reply in Chinese unless the user writes in another language first.", "", "## Tone", "", "- Natural and friendly, not overly formal", "- Concise and to the point" ].join("\n") + "\n", "utf-8"); log("Seeded SOUL.md"); } var mem = path.join(WORKSPACE, "MEMORY.md"); if (!fs.existsSync(mem)) { fs.writeFileSync(mem, [ "# Long-term Memory", "", "" ].join("\n") + "\n", "utf-8"); log("Seeded MEMORY.md"); } } // ---------------------------------------------------------------- // Apply all env-derived patches to a config object // ---------------------------------------------------------------- function applyEnvPatches(config, tgToken) { // Auth config.gateway = config.gateway || {}; config.gateway.auth = gatewayToken ? { mode: "token", token: gatewayToken } : { mode: "password", password: gatewayPassword }; // Control UI flags required for HF Spaces iframe embedding config.gateway.controlUi = config.gateway.controlUi || {}; config.gateway.controlUi.allowInsecureAuth = true; config.gateway.controlUi.allowedOrigins = ["*"]; config.gateway.controlUi.dangerouslyDisableDeviceAuth = true; config.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true; // Always refresh trusted proxies (HF pod IPs can change) config.gateway.trustedProxies = trustedProxies; // Agents config.agents = config.agents || {}; config.agents.defaults = config.agents.defaults || {}; config.agents.defaults.workspace = WORKSPACE; if (!config.agents.defaults.model || config.agents.defaults.model === "WILL_BE_SET_BY_ENV") { config.agents.defaults.model = defaultModel; } // Env vars (all provider keys available inside agent tools) config.env = config.env || {}; config.env.vars = {}; providerKeys.forEach(function(k) { config.env.vars[k] = envStr(k); }); // Model provider API keys patchProviderKeys(config); // Telegram channel if (tgToken) { config.channels = config.channels || {}; config.channels.telegram = config.channels.telegram || {}; config.channels.telegram.enabled = true; config.channels.telegram.accounts = config.channels.telegram.accounts || {}; config.channels.telegram.accounts.main = { botToken: tgToken, apiRoot: "https://api.telegram.org" }; } return config; } // ---------------------------------------------------------------- // Main // ---------------------------------------------------------------- (async function() { // Ensure directories exist fs.mkdirSync(STATE_DIR, { recursive: true }); fs.mkdirSync(WORKSPACE, { recursive: true }); fs.mkdirSync(path.join(WORKSPACE, "memory"), { recursive: true }); // Seed workspace on first boot seedWorkspace(); // Telegram webhook var tgToken = envStr("TELEGRAM_BOT_TOKEN"); if (tgToken) { await setupWebhook(tgToken); } else { log("Telegram: disabled (no TELEGRAM_BOT_TOKEN)"); } // Load base config // Standard OpenClaw pattern: the bucket config is the single source // of truth. It contains everything: models, channels, telegram bindings, // devices, credentials. It persists across all restarts unchanged. // The template (uploaded openclaw.json) is only used on first boot // when no bucket config exists yet. // // To update models/providers after first boot: use the Control UI. // Changes are saved directly to the bucket config automatically. var config = null; var mode = ""; // Primary: bucket config (preserves ALL user settings across restarts) if (fs.existsSync(CONFIG_PATH)) { try { config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8").trim()); mode = "bucket (full config preserved)"; } catch(e) { log("Bucket config unreadable (" + e.message + ") - falling back to template"); } } // First boot only: use template to seed the bucket if (!config && fs.existsSync(TEMPLATE)) { try { config = JSON.parse(fs.readFileSync(TEMPLATE, "utf-8").trim()); mode = "template (first boot - seeding bucket)"; } catch(e) { log("Template unreadable (" + e.message + ")"); } } if (!config) { config = { gateway: {}, agents: { defaults: {} }, env: { vars: {} } }; mode = "fresh (no template found)"; } // Apply env patches config = applyEnvPatches(config, tgToken); // Atomic write: backup then overwrite if (fs.existsSync(CONFIG_PATH)) { fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + ".bak"); } fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8"); // Summary var provCount = config.models && config.models.providers ? Object.keys(config.models.providers).length : 0; var modelCount = config.models && config.models.providers ? Object.values(config.models.providers).reduce(function(n, p) { return n + (p.models ? p.models.length : 0); }, 0) : 0; log("Done. Mode: " + mode); log(" auth = " + (gatewayToken ? "token" : "password")); log(" model = " + (config.agents.defaults.model || defaultModel)); log(" workspace = " + WORKSPACE); log(" proxies = " + trustedProxies.length); log(" env.vars = " + providerKeys.length); log(" providers = " + provCount + " (" + modelCount + " models)"); log(" storage = bucket at /data (100 GB persistent)"); })().catch(function(e) { err("Fatal: " + e.message); process.exit(0); });