Subsequently commited on
Commit
654b09e
·
verified ·
1 Parent(s): f766c15

Update setup-hf-config.mjs

Browse files
Files changed (1) hide show
  1. setup-hf-config.mjs +299 -228
setup-hf-config.mjs CHANGED
@@ -1,232 +1,258 @@
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,68 +261,113 @@ function applyEnvironment(config) {
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
- }
 
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 flags required for HF Spaces iframe embedding
 
 
246
  config.gateway.controlUi = config.gateway.controlUi || {};
247
+ config.gateway.controlUi.allowInsecureAuth = true;
248
+ config.gateway.controlUi.allowedOrigins = ["*"];
249
+ config.gateway.controlUi.dangerouslyDisableDeviceAuth = true;
250
+ config.gateway.controlUi.dangerouslyAllowHostHeaderOriginFallback = true;
 
251
 
252
+ // Always refresh trusted proxies (HF pod IPs can change)
253
  config.gateway.trustedProxies = trustedProxies;
254
 
255
+ // Agents
256
  config.agents = config.agents || {};
257
  config.agents.defaults = config.agents.defaults || {};
258
  config.agents.defaults.workspace = WORKSPACE;
 
261
  config.agents.defaults.model = defaultModel;
262
  }
263
 
264
+ // Env vars (all provider keys available inside agent tools)
265
  config.env = config.env || {};
266
+ config.env.vars = {};
267
+ providerKeys.forEach(function(k) { config.env.vars[k] = envStr(k); });
268
+
269
+ // Model provider API keys
270
  patchProviderKeys(config);
271
 
272
+ // Telegram channel
273
+ if (tgToken) {
274
  config.channels = config.channels || {};
275
  config.channels.telegram = config.channels.telegram || {};
276
+ config.channels.telegram.enabled = true;
277
  config.channels.telegram.accounts = config.channels.telegram.accounts || {};
 
278
  config.channels.telegram.accounts.main = {
279
+ botToken: tgToken,
280
+ apiRoot: "https://api.telegram.org"
 
281
  };
 
 
 
 
 
 
 
282
  }
283
 
284
  return config;
285
  }
286
 
287
+ // ----------------------------------------------------------------
288
+ // Main
289
+ // ----------------------------------------------------------------
290
+ (async function() {
291
+ // Ensure directories exist
292
+ fs.mkdirSync(STATE_DIR, { recursive: true });
293
+ fs.mkdirSync(WORKSPACE, { recursive: true });
294
+ fs.mkdirSync(path.join(WORKSPACE, "memory"), { recursive: true });
295
+
296
+ // Seed workspace on first boot
297
  seedWorkspace();
298
 
299
+ // Telegram webhook
300
+ var tgToken = envStr("TELEGRAM_BOT_TOKEN");
301
+ if (tgToken) {
302
+ await setupWebhook(tgToken);
303
+ } else {
304
+ log("Telegram: disabled (no TELEGRAM_BOT_TOKEN)");
305
+ }
306
+
307
+ // Load base config
308
+ // Standard OpenClaw pattern: the bucket config is the single source
309
+ // of truth. It contains everything: models, channels, telegram bindings,
310
+ // devices, credentials. It persists across all restarts unchanged.
311
+ // The template (uploaded openclaw.json) is only used on first boot
312
+ // when no bucket config exists yet.
313
+ //
314
+ // To update models/providers after first boot: use the Control UI.
315
+ // Changes are saved directly to the bucket config automatically.
316
+ var config = null;
317
+ var mode = "";
318
 
319
+ // Primary: bucket config (preserves ALL user settings across restarts)
320
+ if (fs.existsSync(CONFIG_PATH)) {
321
+ try {
322
+ config = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8").trim());
323
+ mode = "bucket (full config preserved)";
324
+ } catch(e) {
325
+ log("Bucket config unreadable (" + e.message + ") - falling back to template");
326
+ }
327
+ }
328
+
329
+ // First boot only: use template to seed the bucket
330
+ if (!config && fs.existsSync(TEMPLATE)) {
331
+ try {
332
+ config = JSON.parse(fs.readFileSync(TEMPLATE, "utf-8").trim());
333
+ mode = "template (first boot - seeding bucket)";
334
+ } catch(e) {
335
+ log("Template unreadable (" + e.message + ")");
336
+ }
337
+ }
338
+
339
+ if (!config) {
340
+ config = { gateway: {}, agents: { defaults: {} }, env: { vars: {} } };
341
+ mode = "fresh (no template found)";
342
+ }
343
+
344
+ // Apply env patches
345
+ config = applyEnvPatches(config, tgToken);
346
+
347
+ // Atomic write: backup then overwrite
348
  if (fs.existsSync(CONFIG_PATH)) {
349
  fs.copyFileSync(CONFIG_PATH, CONFIG_PATH + ".bak");
350
  }
351
+ fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2), "utf-8");
352
+
353
+ // Summary
354
+ var provCount = config.models && config.models.providers
355
+ ? Object.keys(config.models.providers).length : 0;
356
+ var modelCount = config.models && config.models.providers
357
+ ? Object.values(config.models.providers).reduce(function(n, p) {
358
+ return n + (p.models ? p.models.length : 0);
359
+ }, 0) : 0;
360
+
361
+ log("Done. Mode: " + mode);
362
+ log(" auth = " + (gatewayToken ? "token" : "password"));
363
+ log(" model = " + (config.agents.defaults.model || defaultModel));
364
+ log(" workspace = " + WORKSPACE);
365
+ log(" proxies = " + trustedProxies.length);
366
+ log(" env.vars = " + providerKeys.length);
367
+ log(" providers = " + provCount + " (" + modelCount + " models)");
368
+ log(" storage = bucket at /data (100 GB persistent)");
369
+
370
+ })().catch(function(e) {
371
+ err("Fatal: " + e.message);
372
+ process.exit(0);
373
+ });