Subsequently commited on
Commit
bb6743a
·
verified ·
1 Parent(s): 87eacef

Update setup-hf-config.mjs

Browse files
Files changed (1) hide show
  1. setup-hf-config.mjs +229 -312
setup-hf-config.mjs CHANGED
@@ -1,270 +1,232 @@
1
- import fs from "node:fs";
2
  import path from "node:path";
3
  import https from "node:https";
4
 
5
- // ============================================================
6
- // OpenClaw HF Spaces - Production Config Writer
7
- //
8
- // Storage: HF Storage Bucket at /data (100 GB persistent).
9
- // All runtime data lives in OPENCLAW_HOME/.openclaw/.
10
- //
11
- // Config priority:
12
- // 1. Existing openclaw.json in bucket -> PATCH env vars only
13
- // 2. /app/openclaw-template.json -> use as base + patch
14
- // 3. Neither -> minimal fresh + patch
15
- //
16
- // Always patched from env vars (never lost on restart):
17
- // gateway.auth, gateway.controlUi, gateway.trustedProxies
18
- // agents.defaults.workspace, agents.defaults.model
19
- // env.vars, models.providers.*.apiKey, channels.telegram
20
- // ============================================================
21
 
22
- var HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/home/user";
23
- var STATE_DIR = path.join(HOME, ".openclaw");
24
- var CONFIG_PATH = path.join(STATE_DIR, "openclaw.json");
25
- var WORKSPACE = path.join(STATE_DIR, "workspace");
26
- var SPACE_HOST = (process.env.SPACE_HOST || "").trim();
27
- var TEMPLATE = "/app/openclaw-template.json";
28
 
29
- function log(msg) { console.log("[setup] " + msg); }
30
- function err(msg) { console.error("[setup] ERROR: " + msg); }
31
-
32
- log("Starting... HOME=" + HOME);
33
-
34
- // ----------------------------------------------------------------
35
- // Helpers
36
- // ----------------------------------------------------------------
37
- function envStr(key) { return (process.env[key] || "").trim(); }
38
-
39
- function parseList(val) {
40
- if (!val || !val.trim()) return [];
41
- return val.split(",").map(function(s){ return s.trim(); }).filter(Boolean);
42
  }
43
 
44
- // ----------------------------------------------------------------
45
- // Auth (required)
46
- // ----------------------------------------------------------------
47
- var gatewayToken = envStr("OPENCLAW_GATEWAY_TOKEN");
48
- var gatewayPassword = envStr("OPENCLAW_GATEWAY_PASSWORD");
49
  if (!gatewayToken && !gatewayPassword) {
50
- err("FATAL: set OPENCLAW_GATEWAY_TOKEN in Secrets");
51
- process.exit(0);
 
 
52
  }
53
 
54
- // ----------------------------------------------------------------
55
- // Default model
56
- // ----------------------------------------------------------------
57
- var defaultModel = envStr("OPENCLAW_HF_DEFAULT_MODEL") || "google/gemini-2.0-flash";
58
 
59
- // ----------------------------------------------------------------
60
- // Provider API key detection
61
- // ----------------------------------------------------------------
62
- var EXCLUDE_PREFIXES = [
63
- "OPENCLAW_", "SPACE_", "SYSTEM_", "HF_",
64
- "NODE_", "PATH", "HOME", "USER", "PWD", "LANG", "LC_",
65
- "npm_", "HOSTNAME", "SHELL", "TERM", "SHLVL"
66
  ];
67
- var INCLUDE_SUFFIXES = [
68
- "_API_KEY", "_SECRET_KEY", "_ACCESS_TOKEN",
69
- "_BOT_TOKEN", "_AUTH_TOKEN", "_APP_KEY"
70
  ];
71
-
72
- function isProviderKey(k) {
73
- var i;
74
- for (i = 0; i < EXCLUDE_PREFIXES.length; i++) {
75
- if (k.indexOf(EXCLUDE_PREFIXES[i]) === 0) return false;
76
- }
77
- for (i = 0; i < INCLUDE_SUFFIXES.length; i++) {
78
- var s = INCLUDE_SUFFIXES[i];
79
- if (k.length > s.length && k.indexOf(s) === k.length - s.length) return true;
80
- }
81
- return false;
82
  }
83
-
84
- var providerKeys = Object.keys(process.env).filter(function(k) {
85
- return isProviderKey(k) && envStr(k);
86
- }).sort();
87
-
88
- log("Provider keys (" + providerKeys.length + "): " + providerKeys.join(", "));
89
-
90
- // ----------------------------------------------------------------
91
- // Trusted proxies (HF internal + standard RFC1918)
92
- // ----------------------------------------------------------------
93
- var envProxies = parseList(envStr("OPENCLAW_GATEWAY_TRUSTED_PROXIES"));
94
- var trustedProxies = envProxies.length > 0 ? envProxies : [
95
- "10.0.0.0/8", "172.16.0.0/12", "192.168.0.0/16",
96
- "10.16.0.0/12", "10.20.0.0/12",
97
- "10.16.4.123", "10.16.7.92", "10.16.18.232",
98
- "10.16.34.155", "10.16.43.133", "10.16.1.206",
99
- "10.16.37.110", "10.16.43.246",
100
- "10.20.1.9", "10.20.1.222", "10.20.26.157", "10.20.31.87",
101
- "10.20.0.1", "172.17.0.1", "127.0.0.1"
102
- ];
103
-
104
- // ----------------------------------------------------------------
105
- // Provider -> env var mapping
106
- // ----------------------------------------------------------------
107
- var PROVIDER_KEY_MAP = {
108
- "openrouter": "OPENROUTER_API_KEY",
109
- "siliconflow": "SILICONFLOW_API_KEY",
110
- "deepseek": "DEEPSEEK_API_KEY",
111
- "groq": "GROQ_API_KEY",
112
- "google": "GOOGLE_API_KEY",
113
- "gemini": "GEMINI_API_KEY",
114
- "minimax": "MINIMAX_API_KEY",
115
- "zai": "ZAI_API_KEY",
116
- "github": "GITHUB_MODELS_API_KEY",
117
  "github-copilot": "GITHUB_MODELS_API_KEY",
118
- "openai": "OPENAI_API_KEY",
119
- "anthropic": "ANTHROPIC_API_KEY"
120
  };
121
 
 
 
 
 
 
 
 
 
 
 
 
122
  function patchProviderKeys(config) {
123
- if (!config.models || !config.models.providers) return;
124
- var providers = config.models.providers;
125
- Object.keys(providers).forEach(function(name) {
126
- var lower = name.toLowerCase();
127
- var envKey = PROVIDER_KEY_MAP[lower];
128
- var val = envKey ? envStr(envKey) : "";
129
- if (!val) {
130
- // fuzzy match: find any provider key whose name contains the provider name
131
- var found = providerKeys.find(function(k) {
132
- return k.toLowerCase().indexOf(lower) >= 0;
133
- });
134
- if (found) val = envStr(found);
135
  }
136
- if (val) {
137
- providers[name].apiKey = val;
138
- log("Provider key set: " + name + " <- " + (envKey || "fuzzy"));
139
- } else if (providers[name].apiKey === "WILL_BE_SET_BY_ENV") {
140
- delete providers[name].apiKey;
141
- log("Provider key missing (skipped): " + name);
 
142
  }
143
- });
144
  }
145
 
146
- // ----------------------------------------------------------------
147
- // Telegram webhook helpers
148
- // ----------------------------------------------------------------
149
- function tgRequest(token, method, body) {
150
- return new Promise(function(resolve) {
151
- var data = JSON.stringify(body || {});
152
- var req = https.request({
153
  hostname: "api.telegram.org",
154
- path: "/bot" + token + "/" + method,
155
- method: "POST",
156
- headers: {
157
- "Content-Type": "application/json",
158
- "Content-Length": Buffer.byteLength(data)
159
  }
160
- }, function(res) {
161
- var buf = "";
162
- res.on("data", function(c) { buf += c; });
163
- res.on("end", function() {
164
- try { resolve(JSON.parse(buf)); } catch(e) { resolve(null); }
 
 
 
 
 
 
 
 
 
 
165
  });
166
  });
167
- req.on("error", function() { resolve(null); });
168
- req.setTimeout(10000, function() { req.destroy(); resolve(null); });
169
- req.write(data);
 
 
 
 
 
 
 
170
  req.end();
171
  });
172
  }
173
 
174
- async function setupWebhook(token) {
175
- if (!SPACE_HOST) {
176
- log("Telegram: no SPACE_HOST - webhook skipped");
177
- return;
178
- }
179
- var targetUrl = "https://" + SPACE_HOST + "/tg-webhook";
180
-
181
- // Check if webhook already points to our URL - skip if already set
182
- var info = await tgRequest(token, "getWebhookInfo", {});
183
- if (info && info.ok && info.result && info.result.url === targetUrl) {
184
- log("Telegram: webhook already set -> " + targetUrl);
185
- return;
 
 
 
 
 
186
  }
187
 
188
- var r = await tgRequest(token, "setWebhook", {
189
- url: targetUrl, drop_pending_updates: true, max_connections: 10
190
- });
191
- if (r && r.ok) {
192
- log("Telegram: webhook registered -> " + targetUrl);
193
- } else {
194
- var desc = r && r.description ? r.description : "unknown error";
195
- log("Telegram: webhook failed (" + desc + ")");
196
- log("Telegram: set manually at:");
197
- log(" https://api.telegram.org/bot" + token + "/setWebhook?url=" + targetUrl + "&drop_pending_updates=true");
198
  }
199
  }
200
 
201
- // ----------------------------------------------------------------
202
- // Seed workspace (first boot only - bucket persists after)
203
- // ----------------------------------------------------------------
204
- function seedWorkspace() {
205
- var soul = path.join(WORKSPACE, "SOUL.md");
206
- if (!fs.existsSync(soul)) {
207
- fs.writeFileSync(soul, [
208
- "# Soul",
209
- "",
210
- "You are a helpful, warm, concise AI assistant.",
211
- "",
212
- "## Language",
213
- "",
214
- "Default language: Simplified Chinese.",
215
- "Always reply in Chinese unless the user writes in another language first.",
216
- "",
217
- "## Tone",
218
- "",
219
- "- Natural and friendly, not overly formal",
220
- "- Concise and to the point"
221
- ].join("\n") + "\n", "utf-8");
222
- log("Seeded SOUL.md");
223
  }
224
- var mem = path.join(WORKSPACE, "MEMORY.md");
225
- if (!fs.existsSync(mem)) {
226
- fs.writeFileSync(mem, [
227
- "# Long-term Memory",
228
- "",
229
- "<!-- OpenClaw writes important facts here. -->"
230
- ].join("\n") + "\n", "utf-8");
231
- log("Seeded MEMORY.md");
 
232
  }
 
 
 
 
 
 
233
  }
234
 
235
- // ----------------------------------------------------------------
236
- // Apply all env-derived patches to a config object
237
- // ----------------------------------------------------------------
238
- function applyEnvPatches(config, tgToken) {
239
- // Auth
240
  config.gateway = config.gateway || {};
 
 
 
241
  config.gateway.auth = gatewayToken
242
- ? { mode: "token", token: gatewayToken }
243
  : { mode: "password", password: gatewayPassword };
244
 
245
- // Control UI security configuration
246
- config.gateway.controlUi = config.gateway.controlUi || {};
247
-
248
- // Remove retired and unsafe legacy options
249
- delete config.gateway.controlUi.dangerouslyDisableDeviceAuth;
250
- delete config.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback;
 
 
 
251
 
252
- // HTTPS is provided by Hugging Face Spaces
253
- delete config.gateway.controlUi.allowInsecureAuth;
254
-
255
- // Restrict browser origin to this Space
256
- if (SPACE_HOST) {
257
- config.gateway.controlUi.allowedOrigins = [
258
- "https://" + SPACE_HOST
259
- ];
260
- } else {
261
- delete config.gateway.controlUi.allowedOrigins;
262
- }
263
-
264
- // Always refresh trusted proxies (HF pod IPs can change)
265
  config.gateway.trustedProxies = trustedProxies;
266
 
267
- // Agents
268
  config.agents = config.agents || {};
269
  config.agents.defaults = config.agents.defaults || {};
270
  config.agents.defaults.workspace = WORKSPACE;
@@ -273,113 +235,68 @@ if (SPACE_HOST) {
273
  config.agents.defaults.model = defaultModel;
274
  }
275
 
276
- // Env vars (all provider keys available inside agent tools)
277
  config.env = config.env || {};
278
- config.env.vars = {};
279
- providerKeys.forEach(function(k) { config.env.vars[k] = envStr(k); });
280
-
281
- // Model provider API keys
282
  patchProviderKeys(config);
283
 
284
- // Telegram channel
285
- if (tgToken) {
286
  config.channels = config.channels || {};
287
  config.channels.telegram = config.channels.telegram || {};
288
- config.channels.telegram.enabled = true;
289
  config.channels.telegram.accounts = config.channels.telegram.accounts || {};
 
290
  config.channels.telegram.accounts.main = {
291
- botToken: tgToken,
292
- apiRoot: "https://api.telegram.org"
 
293
  };
 
 
 
 
 
 
 
294
  }
295
 
296
  return config;
297
  }
298
 
299
- // ----------------------------------------------------------------
300
- // Main
301
- // ----------------------------------------------------------------
302
- (async function() {
303
- // Ensure directories exist
304
- fs.mkdirSync(STATE_DIR, { recursive: true });
305
- fs.mkdirSync(WORKSPACE, { recursive: true });
306
- fs.mkdirSync(path.join(WORKSPACE, "memory"), { recursive: true });
307
-
308
- // Seed workspace on first boot
309
  seedWorkspace();
310
 
311
- // Telegram webhook
312
- var tgToken = envStr("TELEGRAM_BOT_TOKEN");
313
- if (tgToken) {
314
- await setupWebhook(tgToken);
315
- } else {
316
- log("Telegram: disabled (no TELEGRAM_BOT_TOKEN)");
317
- }
318
-
319
- // Load base config
320
- // Standard OpenClaw pattern: the bucket config is the single source
321
- // of truth. It contains everything: models, channels, telegram bindings,
322
- // devices, credentials. It persists across all restarts unchanged.
323
- // The template (uploaded openclaw.json) is only used on first boot
324
- // when no bucket config exists yet.
325
- //
326
- // To update models/providers after first boot: use the Control UI.
327
- // Changes are saved directly to the bucket config automatically.
328
- var config = null;
329
- var mode = "";
330
 
331
- // Primary: bucket config (preserves ALL user settings across restarts)
332
- if (fs.existsSync(CONFIG_PATH)) {
333
- try {
334
- config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8").trim());
335
- mode = "bucket (full config preserved)";
336
- } catch(e) {
337
- log("Bucket config unreadable (" + e.message + ") - falling back to template");
338
- }
339
- }
340
-
341
- // First boot only: use template to seed the bucket
342
- if (!config && fs.existsSync(TEMPLATE)) {
343
- try {
344
- config = JSON.parse(fs.readFileSync(TEMPLATE, "utf-8").trim());
345
- mode = "template (first boot - seeding bucket)";
346
- } catch(e) {
347
- log("Template unreadable (" + e.message + ")");
348
- }
349
- }
350
-
351
- if (!config) {
352
- config = { gateway: {}, agents: { defaults: {} }, env: { vars: {} } };
353
- mode = "fresh (no template found)";
354
- }
355
-
356
- // Apply env patches
357
- config = applyEnvPatches(config, tgToken);
358
-
359
- // Atomic write: backup then overwrite
360
  if (fs.existsSync(CONFIG_PATH)) {
361
  fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + ".bak");
362
  }
363
- fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
364
-
365
- // Summary
366
- var provCount = config.models && config.models.providers
367
- ? Object.keys(config.models.providers).length : 0;
368
- var modelCount = config.models && config.models.providers
369
- ? Object.values(config.models.providers).reduce(function(n, p) {
370
- return n + (p.models ? p.models.length : 0);
371
- }, 0) : 0;
372
-
373
- log("Done. Mode: " + mode);
374
- log(" auth = " + (gatewayToken ? "token" : "password"));
375
- log(" model = " + (config.agents.defaults.model || defaultModel));
376
- log(" workspace = " + WORKSPACE);
377
- log(" proxies = " + trustedProxies.length);
378
- log(" env.vars = " + providerKeys.length);
379
- log(" providers = " + provCount + " (" + modelCount + " models)");
380
- log(" storage = bucket at /data (100 GB persistent)");
381
-
382
- })().catch(function(e) {
383
- err("Fatal: " + e.message);
384
- process.exit(0);
385
- });
 
1
+ import fs from "node:fs";
2
  import path from "node:path";
3
  import https from "node:https";
4
 
5
+ // OpenClaw / Hugging Face Spaces configuration writer.
6
+ // Persistent state: /data/.openclaw (OPENCLAW_HOME=/data).
7
+ // Telegram uses OpenClaw's default long-polling transport.
8
+ // Device pairing is deliberately left to OpenClaw's supported pairing flow.
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ const HOME = process.env.OPENCLAW_HOME || process.env.HOME || "/data";
11
+ const STATE_DIR = path.join(HOME, ".openclaw");
12
+ const CONFIG_PATH = path.join(STATE_DIR, "openclaw.json");
13
+ const WORKSPACE = path.join(STATE_DIR, "workspace");
14
+ const TEMPLATE = "/app/openclaw-template.json";
15
+ const SPACE_HOST = (process.env.SPACE_HOST || "").trim().replace(/^https?:\/\//, "").replace(/\/+$/, "");
16
 
17
+ function log(message) {
18
+ console.log("[setup] " + message);
19
+ }
20
+ function fatal(message) {
21
+ console.error("[setup] FATAL: " + message);
22
+ process.exit(1);
23
+ }
24
+ function envStr(key) {
25
+ return (process.env[key] || "").trim();
26
+ }
27
+ function parseList(value) {
28
+ return value ? value.split(",").map((x) => x.trim()).filter(Boolean) : [];
 
29
  }
30
 
31
+ // Require explicit Gateway authentication. Token is preferred.
32
+ const gatewayToken = envStr("OPENCLAW_GATEWAY_TOKEN");
33
+ const gatewayPassword = envStr("OPENCLAW_GATEWAY_PASSWORD");
 
 
34
  if (!gatewayToken && !gatewayPassword) {
35
+ fatal("Set OPENCLAW_GATEWAY_TOKEN (recommended) or OPENCLAW_GATEWAY_PASSWORD in Space Secrets.");
36
+ }
37
+ if (!SPACE_HOST) {
38
+ fatal("SPACE_HOST is empty. Confirm this is running as a Hugging Face Space.");
39
  }
40
 
41
+ const publicOrigin = "https://" + SPACE_HOST;
42
+ const defaultModel = envStr("OPENCLAW_HF_DEFAULT_MODEL") || "google/gemini-2.0-flash";
 
 
43
 
44
+ // Provider API key discovery, preserving the original project's naming convention.
45
+ const EXCLUDE_PREFIXES = [
46
+ "OPENCLAW_", "SPACE_", "SYSTEM_", "HF_", "NODE_", "PATH", "HOME",
47
+ "USER", "PWD", "LANG", "LC_", "npm_", "HOSTNAME", "SHELL", "TERM", "SHLVL"
 
 
 
48
  ];
49
+ const INCLUDE_SUFFIXES = [
50
+ "_API_KEY", "_SECRET_KEY", "_ACCESS_TOKEN", "_BOT_TOKEN", "_AUTH_TOKEN", "_APP_KEY"
 
51
  ];
52
+ function isProviderKey(key) {
53
+ if (EXCLUDE_PREFIXES.some((prefix) => key.startsWith(prefix))) return false;
54
+ return INCLUDE_SUFFIXES.some((suffix) => key.length > suffix.length && key.endsWith(suffix));
 
 
 
 
 
 
 
 
55
  }
56
+ const providerKeys = Object.keys(process.env)
57
+ .filter((key) => isProviderKey(key) && envStr(key))
58
+ .sort();
59
+
60
+ const PROVIDER_KEY_MAP = {
61
+ openrouter: "OPENROUTER_API_KEY",
62
+ siliconflow: "SILICONFLOW_API_KEY",
63
+ deepseek: "DEEPSEEK_API_KEY",
64
+ groq: "GROQ_API_KEY",
65
+ google: "GOOGLE_API_KEY",
66
+ gemini: "GEMINI_API_KEY",
67
+ minimax: "MINIMAX_API_KEY",
68
+ zai: "ZAI_API_KEY",
69
+ github: "GITHUB_MODELS_API_KEY",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  "github-copilot": "GITHUB_MODELS_API_KEY",
71
+ openai: "OPENAI_API_KEY",
72
+ anthropic: "ANTHROPIC_API_KEY"
73
  };
74
 
75
+ // Prefer explicitly supplied proxy CIDRs. The fallback retains the exact
76
+ // HF-internal addresses already present in the supplied deployment file,
77
+ // but removes broad RFC1918 trust ranges such as 10.0.0.0/8.
78
+ const envProxies = parseList(envStr("OPENCLAW_GATEWAY_TRUSTED_PROXIES"));
79
+ const trustedProxies = envProxies.length ? envProxies : [
80
+ "10.16.4.123", "10.16.7.92", "10.16.18.232", "10.16.34.155",
81
+ "10.16.43.133", "10.16.1.206", "10.16.37.110", "10.16.43.246",
82
+ "10.20.1.9", "10.20.1.222", "10.20.26.157", "10.20.31.87",
83
+ "10.20.0.1", "172.17.0.1", "127.0.0.1"
84
+ ];
85
+
86
  function patchProviderKeys(config) {
87
+ const providers = config.models && config.models.providers;
88
+ if (!providers || typeof providers !== "object") return;
89
+
90
+ for (const [name, provider] of Object.entries(providers)) {
91
+ if (!provider || typeof provider !== "object") continue;
92
+ const lower = name.toLowerCase();
93
+ const mapped = PROVIDER_KEY_MAP[lower];
94
+ let value = mapped ? envStr(mapped) : "";
95
+
96
+ if (!value) {
97
+ const matched = providerKeys.find((key) => key.toLowerCase().includes(lower));
98
+ if (matched) value = envStr(matched);
99
  }
100
+
101
+ if (value) {
102
+ provider.apiKey = value;
103
+ log("Provider API key configured: " + name);
104
+ } else if (provider.apiKey === "WILL_BE_SET_BY_ENV") {
105
+ delete provider.apiKey;
106
+ log("Provider API key not supplied; placeholder removed: " + name);
107
  }
108
+ }
109
  }
110
 
111
+ function clearTelegramWebhook(token) {
112
+ return new Promise((resolve) => {
113
+ const body = JSON.stringify({ drop_pending_updates: false });
114
+ const req = https.request({
 
 
 
115
  hostname: "api.telegram.org",
116
+ path: "/bot" + token + "/deleteWebhook",
117
+ method: "POST",
118
+ headers: {
119
+ "Content-Type": "application/json",
120
+ "Content-Length": Buffer.byteLength(body)
121
  }
122
+ }, (res) => {
123
+ let data = "";
124
+ res.on("data", (chunk) => { data += chunk; });
125
+ res.on("end", () => {
126
+ try {
127
+ const result = JSON.parse(data);
128
+ if (result && result.ok) {
129
+ log("Removed any old Telegram webhook; pending updates were preserved.");
130
+ } else {
131
+ log("WARNING: Telegram deleteWebhook returned: " + (result?.description || "unknown response"));
132
+ }
133
+ } catch {
134
+ log("WARNING: Could not parse Telegram deleteWebhook response.");
135
+ }
136
+ resolve();
137
  });
138
  });
139
+ req.setTimeout(10000, () => {
140
+ req.destroy();
141
+ log("WARNING: Telegram deleteWebhook timed out; polling may remain blocked by an old webhook.");
142
+ resolve();
143
+ });
144
+ req.on("error", (error) => {
145
+ log("WARNING: Telegram deleteWebhook failed: " + error.message);
146
+ resolve();
147
+ });
148
+ req.write(body);
149
  req.end();
150
  });
151
  }
152
 
153
+ function seedWorkspace() {
154
+ fs.mkdirSync(WORKSPACE, { recursive: true });
155
+ fs.mkdirSync(path.join(WORKSPACE, "memory"), { recursive: true });
156
+
157
+ const soulPath = path.join(WORKSPACE, "SOUL.md");
158
+ if (!fs.existsSync(soulPath)) {
159
+ fs.writeFileSync(soulPath, [
160
+ "# Soul", "",
161
+ "You are a helpful, warm, concise AI assistant.", "",
162
+ "## Language", "",
163
+ "Default language: Simplified Chinese.",
164
+ "Always reply in Chinese unless the user writes in another language first.", "",
165
+ "## Tone", "",
166
+ "- Natural and friendly, not overly formal",
167
+ "- Concise and to the point"
168
+ ].join("\n") + "\n", { encoding: "utf8", mode: 0o600 });
169
+ log("Created workspace/SOUL.md");
170
  }
171
 
172
+ const memoryPath = path.join(WORKSPACE, "MEMORY.md");
173
+ if (!fs.existsSync(memoryPath)) {
174
+ fs.writeFileSync(memoryPath, "# Long-term Memory\n\n<!-- OpenClaw writes important facts here. -->\n",
175
+ { encoding: "utf8", mode: 0o600 });
176
+ log("Created workspace/MEMORY.md");
 
 
 
 
 
177
  }
178
  }
179
 
180
+ function loadBaseConfig() {
181
+ if (fs.existsSync(CONFIG_PATH)) {
182
+ try {
183
+ const parsed = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf8"));
184
+ log("Loaded persistent Bucket config.");
185
+ return { config: parsed, source: "bucket" };
186
+ } catch (error) {
187
+ // Never overwrite a corrupt persistent config silently.
188
+ fatal("Persistent config is invalid JSON (" + error.message + "). Backup it and repair it before restarting.");
189
+ }
 
 
 
 
 
 
 
 
 
 
 
 
190
  }
191
+
192
+ if (fs.existsSync(TEMPLATE)) {
193
+ try {
194
+ const parsed = JSON.parse(fs.readFileSync(TEMPLATE, "utf8"));
195
+ log("Loaded openclaw.json template for first boot.");
196
+ return { config: parsed, source: "template" };
197
+ } catch (error) {
198
+ fatal("Template JSON is invalid (" + error.message + ").");
199
+ }
200
  }
201
+
202
+ log("No template found; creating a minimal first-boot config.");
203
+ return {
204
+ config: { gateway: {}, agents: { defaults: {} }, env: { vars: {} } },
205
+ source: "fresh"
206
+ };
207
  }
208
 
209
+ function applyEnvironment(config) {
 
 
 
 
210
  config.gateway = config.gateway || {};
211
+ config.gateway.mode = "local";
212
+ config.gateway.port = 7860;
213
+ config.gateway.bind = "lan";
214
  config.gateway.auth = gatewayToken
215
+ ? { mode: "token", token: gatewayToken }
216
  : { mode: "password", password: gatewayPassword };
217
 
218
+ // Control UI: allow only this Space's HTTPS origin.
219
+ // Keep device authentication enabled; do not use insecure bypass flags.
220
+ config.gateway.publicOrigin = publicOrigin;
221
+ config.gateway.controlUi = config.gateway.controlUi || {};
222
+ config.gateway.controlUi.enabled = true;
223
+ config.gateway.controlUi.allowedOrigins = [publicOrigin];
224
+ config.gateway.controlUi.allowInsecureAuth = false;
225
+ delete config.gateway.controlUi.dangerouslyDisableDeviceAuth;
226
+ delete config.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback;
227
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  config.gateway.trustedProxies = trustedProxies;
229
 
 
230
  config.agents = config.agents || {};
231
  config.agents.defaults = config.agents.defaults || {};
232
  config.agents.defaults.workspace = WORKSPACE;
 
235
  config.agents.defaults.model = defaultModel;
236
  }
237
 
 
238
  config.env = config.env || {};
239
+ config.env.vars = config.env.vars || {};
240
+ for (const key of providerKeys) config.env.vars[key] = envStr(key);
 
 
241
  patchProviderKeys(config);
242
 
243
+ const telegramToken = envStr("TELEGRAM_BOT_TOKEN");
244
+ if (telegramToken) {
245
  config.channels = config.channels || {};
246
  config.channels.telegram = config.channels.telegram || {};
247
+ config.channels.telegram.enabled = true;
248
  config.channels.telegram.accounts = config.channels.telegram.accounts || {};
249
+ const existingMain = config.channels.telegram.accounts.main || {};
250
  config.channels.telegram.accounts.main = {
251
+ ...existingMain,
252
+ botToken: telegramToken,
253
+ apiRoot: existingMain.apiRoot || "https://api.telegram.org"
254
  };
255
+ // Do not set webhookUrl: long polling is OpenClaw's default transport.
256
+ delete config.channels.telegram.webhookUrl;
257
+ delete config.channels.telegram.webhookSecret;
258
+ delete config.channels.telegram.webhookPath;
259
+ log("Telegram configured for default long polling.");
260
+ } else {
261
+ log("TELEGRAM_BOT_TOKEN not set; existing Telegram config, if any, is preserved.");
262
  }
263
 
264
  return config;
265
  }
266
 
267
+ try {
268
+ fs.mkdirSync(STATE_DIR, { recursive: true });
 
 
 
 
 
 
 
 
269
  seedWorkspace();
270
 
271
+ const { config, source } = loadBaseConfig();
272
+ const telegramToken = envStr("TELEGRAM_BOT_TOKEN");
273
+ if (telegramToken) await clearTelegramWebhook(telegramToken);
274
+ const patched = applyEnvironment(config);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
 
276
+ const tempPath = CONFIG_PATH + ".tmp";
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
277
  if (fs.existsSync(CONFIG_PATH)) {
278
  fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + ".bak");
279
  }
280
+ fs.writeFileSync(tempPath, JSON.stringify(patched, null, 2) + "\n",
281
+ { encoding: "utf8", mode: 0o600 });
282
+ fs.renameSync(tempPath, CONFIG_PATH);
283
+ try { fs.chmodSync(CONFIG_PATH, 0o600); } catch {}
284
+
285
+ const providerCount = patched.models && patched.models.providers
286
+ ? Object.keys(patched.models.providers).length : 0;
287
+ const modelCount = patched.models && patched.models.providers
288
+ ? Object.values(patched.models.providers).reduce(
289
+ (total, provider) => total + (Array.isArray(provider.models) ? provider.models.length : 0), 0)
290
+ : 0;
291
+
292
+ log("Config written successfully. source=" + source);
293
+ log("Gateway auth=" + (gatewayToken ? "token" : "password"));
294
+ log("Gateway origin=" + publicOrigin);
295
+ log("Workspace=" + WORKSPACE);
296
+ log("Trusted proxy entries=" + trustedProxies.length);
297
+ log("Provider keys detected=" + providerKeys.length);
298
+ log("Providers=" + providerCount + "; models=" + modelCount);
299
+ log("Persistent state=" + STATE_DIR);
300
+ } catch (error) {
301
+ fatal(error && error.stack ? error.stack : String(error));
302
+ }