@chrysb/alphaclaw 0.5.1 → 0.5.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -12,6 +12,7 @@ const {
12
12
  } = require("./github");
13
13
  const {
14
14
  buildOnboardArgs,
15
+ writeManagedImportOpenclawConfig,
15
16
  writeSanitizedOpenclawConfig,
16
17
  } = require("./openclaw");
17
18
  const {
@@ -28,6 +29,7 @@ const { installGogCliSkill } = require("../gog-skill");
28
29
 
29
30
  const kPlaceholderEnvValue = "placeholder";
30
31
  const kEnvRefPattern = /\$\{([A-Z_][A-Z0-9_]*)\}/g;
32
+ const kImportedPairingKeys = ["allowFrom", "groupAllowFrom"];
31
33
 
32
34
  const upsertEnvVar = (items, key, value) => {
33
35
  const normalizedKey = String(key || "").trim();
@@ -42,6 +44,65 @@ const upsertEnvVar = (items, key, value) => {
42
44
  return items;
43
45
  };
44
46
 
47
+ const clearImportedChannelPairingState = (channelsRoot) => {
48
+ if (!channelsRoot || typeof channelsRoot !== "object") return false;
49
+ let changed = false;
50
+ for (const [channelKey, channelConfig] of Object.entries(channelsRoot)) {
51
+ if (!channelConfig || typeof channelConfig !== "object") continue;
52
+ if (
53
+ channelKey === "telegram" &&
54
+ Object.prototype.hasOwnProperty.call(channelConfig, "accounts")
55
+ ) {
56
+ delete channelConfig.accounts;
57
+ changed = true;
58
+ }
59
+ for (const pairingKey of kImportedPairingKeys) {
60
+ if (
61
+ Object.prototype.hasOwnProperty.call(channelConfig, pairingKey) &&
62
+ (!Array.isArray(channelConfig[pairingKey]) ||
63
+ channelConfig[pairingKey].length > 0)
64
+ ) {
65
+ channelConfig[pairingKey] = [];
66
+ changed = true;
67
+ }
68
+ }
69
+ if (
70
+ channelConfig.dmPolicy === "allowlist" &&
71
+ (!Array.isArray(channelConfig.allowFrom) ||
72
+ channelConfig.allowFrom.length === 0)
73
+ ) {
74
+ channelConfig.dmPolicy = "pairing";
75
+ changed = true;
76
+ }
77
+ }
78
+ return changed;
79
+ };
80
+
81
+ const clearImportedCredentialPairings = ({ fs, openclawDir }) => {
82
+ const credentialsDir = path.join(openclawDir, "credentials");
83
+ if (!fs.existsSync(credentialsDir)) return;
84
+ let entries = [];
85
+ try {
86
+ entries = fs.readdirSync(credentialsDir);
87
+ } catch {
88
+ return;
89
+ }
90
+ for (const entry of entries) {
91
+ const fileName = typeof entry === "string" ? entry : entry?.name;
92
+ if (!fileName || !fileName.endsWith("-allowFrom.json")) continue;
93
+ const filePath = path.join(credentialsDir, fileName);
94
+ try {
95
+ const parsed = JSON.parse(fs.readFileSync(filePath, "utf8"));
96
+ if (!parsed || typeof parsed !== "object") continue;
97
+ if (Array.isArray(parsed.allowFrom) && parsed.allowFrom.length === 0) {
98
+ continue;
99
+ }
100
+ parsed.allowFrom = [];
101
+ fs.writeFileSync(filePath, JSON.stringify(parsed, null, 2));
102
+ } catch {}
103
+ }
104
+ };
105
+
45
106
  const collectEnvRefs = (value, found = new Set()) => {
46
107
  if (typeof value === "string") {
47
108
  for (const match of value.matchAll(kEnvRefPattern)) {
@@ -62,6 +123,46 @@ const collectEnvRefs = (value, found = new Set()) => {
62
123
  const getEnvVarValue = (items, key) =>
63
124
  items.find((entry) => entry.key === key)?.value || "";
64
125
 
126
+ const syncApiKeyAuthProfilesFromEnvVars = (authProfiles, envVars = []) => {
127
+ if (!authProfiles?.getEnvVarForApiKeyProvider) return;
128
+ const providers = [
129
+ "anthropic",
130
+ "openai",
131
+ "google",
132
+ "opencode",
133
+ "openrouter",
134
+ "zai",
135
+ "vercel-ai-gateway",
136
+ "kilocode",
137
+ "xai",
138
+ "mistral",
139
+ "cerebras",
140
+ "moonshot",
141
+ "kimi-coding",
142
+ "volcengine",
143
+ "byteplus",
144
+ "synthetic",
145
+ "minimax",
146
+ "voyage",
147
+ "groq",
148
+ "deepgram",
149
+ "vllm",
150
+ ];
151
+ const envMap = new Map(
152
+ (envVars || []).map((entry) => [
153
+ String(entry?.key || "").trim(),
154
+ String(entry?.value || ""),
155
+ ]),
156
+ );
157
+ for (const provider of providers) {
158
+ const envKey = authProfiles.getEnvVarForApiKeyProvider(provider);
159
+ if (!envKey) continue;
160
+ const value = String(envMap.get(envKey) || "").trim();
161
+ if (!value || value === kPlaceholderEnvValue) continue;
162
+ authProfiles.upsertApiKeyProfileForEnvVar?.(provider, value);
163
+ }
164
+ };
165
+
65
166
  const buildPlaceholderReview = ({
66
167
  referencedEnvVars,
67
168
  envVars = [],
@@ -115,6 +216,15 @@ const normalizeImportedConfig = ({ fs, openclawDir }) => {
115
216
  };
116
217
  changed = true;
117
218
  }
219
+ const currentWebhookToken = String(cfg?.hooks?.token || "").trim();
220
+ const expectedWebhookTokenRef = "${WEBHOOK_TOKEN}";
221
+ if (cfg.hooks && currentWebhookToken !== expectedWebhookTokenRef) {
222
+ cfg.hooks = {
223
+ ...(cfg.hooks || {}),
224
+ token: expectedWebhookTokenRef,
225
+ };
226
+ changed = true;
227
+ }
118
228
  if (
119
229
  cfg.hooks &&
120
230
  Object.prototype.hasOwnProperty.call(cfg.hooks, "transformsDir")
@@ -124,10 +234,19 @@ const normalizeImportedConfig = ({ fs, openclawDir }) => {
124
234
  cfg.hooks = nextHooks;
125
235
  changed = true;
126
236
  }
237
+ const configFileName = path.basename(configPath).toLowerCase();
238
+ const channelsRoot =
239
+ cfg.channels && typeof cfg.channels === "object"
240
+ ? cfg.channels
241
+ : configFileName.includes("channel")
242
+ ? cfg
243
+ : null;
244
+ changed = clearImportedChannelPairingState(channelsRoot) || changed;
127
245
  if (changed) {
128
246
  fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
129
247
  }
130
248
  }
249
+ clearImportedCredentialPairings({ fs, openclawDir });
131
250
  };
132
251
 
133
252
  const getImportedConfigEnvRefs = ({ fs, openclawDir }) => {
@@ -269,6 +388,7 @@ const createOnboardingService = ({
269
388
  }
270
389
  writeEnvFile(varsToSave);
271
390
  reloadEnv();
391
+ syncApiKeyAuthProfilesFromEnvVars(authProfiles, varsToSave);
272
392
 
273
393
  const [, repoName] = repoUrl.split("/");
274
394
  const repoCheck = await ensureGithubRepoAccessible({
@@ -368,6 +488,12 @@ const createOnboardingService = ({
368
488
 
369
489
  if (!existingConfigPresent) {
370
490
  writeSanitizedOpenclawConfig({ fs, openclawDir: OPENCLAW_DIR, varMap });
491
+ } else if (importMode) {
492
+ writeManagedImportOpenclawConfig({
493
+ fs,
494
+ openclawDir: OPENCLAW_DIR,
495
+ varMap,
496
+ });
371
497
  }
372
498
  authProfiles?.syncConfigAuthReferencesForAgent?.();
373
499
  ensureGatewayProxyConfig(getBaseUrl(req));
@@ -9,6 +9,10 @@ const kUsageTrackerPluginPath = path.resolve(
9
9
  "usage-tracker",
10
10
  );
11
11
  const kDefaultToolsProfile = "full";
12
+ const kBootstrapExtraFiles = [
13
+ "hooks/bootstrap/AGENTS.md",
14
+ "hooks/bootstrap/TOOLS.md",
15
+ ];
12
16
 
13
17
  const buildOnboardArgs = ({
14
18
  varMap,
@@ -126,11 +130,16 @@ const buildOnboardArgs = ({
126
130
  return onboardArgs;
127
131
  };
128
132
 
129
- const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
130
- const configPath = `${openclawDir}/openclaw.json`;
131
- const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
133
+ const ensurePluginAllowed = (cfg, pluginKey) => {
134
+ if (!cfg.plugins.allow.includes(pluginKey)) {
135
+ cfg.plugins.allow.push(pluginKey);
136
+ }
137
+ };
138
+
139
+ const ensureManagedConfigShell = (cfg) => {
132
140
  if (!cfg.channels) cfg.channels = {};
133
141
  if (!cfg.plugins) cfg.plugins = {};
142
+ if (!Array.isArray(cfg.plugins.allow)) cfg.plugins.allow = [];
134
143
  if (!cfg.plugins.load) cfg.plugins.load = {};
135
144
  if (!Array.isArray(cfg.plugins.load.paths)) cfg.plugins.load.paths = [];
136
145
  if (!cfg.plugins.entries) cfg.plugins.entries = {};
@@ -145,9 +154,22 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
145
154
  cfg.hooks.internal.entries["bootstrap-extra-files"] = {
146
155
  ...(cfg.hooks.internal.entries["bootstrap-extra-files"] || {}),
147
156
  enabled: true,
148
- paths: ["hooks/bootstrap/AGENTS.md", "hooks/bootstrap/TOOLS.md"],
157
+ paths: kBootstrapExtraFiles,
149
158
  };
159
+ };
160
+
161
+ const getSafeImportedDmPolicy = (channelConfig = {}) => {
162
+ if (
163
+ channelConfig?.dmPolicy === "allowlist" &&
164
+ (!Array.isArray(channelConfig?.allowFrom) ||
165
+ channelConfig.allowFrom.length === 0)
166
+ ) {
167
+ return "pairing";
168
+ }
169
+ return channelConfig?.dmPolicy || "pairing";
170
+ };
150
171
 
172
+ const applyFreshOnboardingChannels = ({ cfg, varMap }) => {
151
173
  if (varMap.TELEGRAM_BOT_TOKEN) {
152
174
  cfg.channels.telegram = {
153
175
  enabled: true,
@@ -156,6 +178,7 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
156
178
  groupPolicy: "allowlist",
157
179
  };
158
180
  cfg.plugins.entries.telegram = { enabled: true };
181
+ ensurePluginAllowed(cfg, "telegram");
159
182
  console.log("[onboard] Telegram configured");
160
183
  }
161
184
  if (varMap.DISCORD_BOT_TOKEN) {
@@ -166,12 +189,21 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
166
189
  groupPolicy: "allowlist",
167
190
  };
168
191
  cfg.plugins.entries.discord = { enabled: true };
192
+ ensurePluginAllowed(cfg, "discord");
169
193
  console.log("[onboard] Discord configured");
170
194
  }
171
195
  if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
172
196
  cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
173
197
  }
198
+ ensurePluginAllowed(cfg, "usage-tracker");
174
199
  cfg.plugins.entries["usage-tracker"] = { enabled: true };
200
+ };
201
+
202
+ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
203
+ const configPath = `${openclawDir}/openclaw.json`;
204
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
205
+ ensureManagedConfigShell(cfg);
206
+ applyFreshOnboardingChannels({ cfg, varMap });
175
207
 
176
208
  let content = JSON.stringify(cfg, null, 2);
177
209
  const replacements = buildSecretReplacements(varMap, process.env);
@@ -192,4 +224,55 @@ const writeSanitizedOpenclawConfig = ({ fs, openclawDir, varMap }) => {
192
224
  console.log("[onboard] Config sanitized");
193
225
  };
194
226
 
195
- module.exports = { buildOnboardArgs, writeSanitizedOpenclawConfig };
227
+ const writeManagedImportOpenclawConfig = ({ fs, openclawDir, varMap }) => {
228
+ const configPath = `${openclawDir}/openclaw.json`;
229
+ const cfg = JSON.parse(fs.readFileSync(configPath, "utf8"));
230
+ ensureManagedConfigShell(cfg);
231
+
232
+ if (!cfg.plugins.load.paths.includes(kUsageTrackerPluginPath)) {
233
+ cfg.plugins.load.paths.push(kUsageTrackerPluginPath);
234
+ }
235
+ ensurePluginAllowed(cfg, "usage-tracker");
236
+ cfg.plugins.entries["usage-tracker"] = {
237
+ ...(cfg.plugins.entries["usage-tracker"] || {}),
238
+ enabled: true,
239
+ };
240
+
241
+ if (varMap.TELEGRAM_BOT_TOKEN) {
242
+ cfg.channels.telegram = {
243
+ ...(cfg.channels.telegram || {}),
244
+ enabled: true,
245
+ botToken: "${TELEGRAM_BOT_TOKEN}",
246
+ dmPolicy: getSafeImportedDmPolicy(cfg.channels.telegram),
247
+ groupPolicy: cfg.channels.telegram?.groupPolicy || "allowlist",
248
+ };
249
+ cfg.plugins.entries.telegram = {
250
+ ...(cfg.plugins.entries.telegram || {}),
251
+ enabled: true,
252
+ };
253
+ ensurePluginAllowed(cfg, "telegram");
254
+ }
255
+
256
+ if (varMap.DISCORD_BOT_TOKEN) {
257
+ cfg.channels.discord = {
258
+ ...(cfg.channels.discord || {}),
259
+ enabled: true,
260
+ token: "${DISCORD_BOT_TOKEN}",
261
+ dmPolicy: getSafeImportedDmPolicy(cfg.channels.discord),
262
+ groupPolicy: cfg.channels.discord?.groupPolicy || "allowlist",
263
+ };
264
+ cfg.plugins.entries.discord = {
265
+ ...(cfg.plugins.entries.discord || {}),
266
+ enabled: true,
267
+ };
268
+ ensurePluginAllowed(cfg, "discord");
269
+ }
270
+
271
+ fs.writeFileSync(configPath, JSON.stringify(cfg, null, 2));
272
+ };
273
+
274
+ module.exports = {
275
+ buildOnboardArgs,
276
+ writeManagedImportOpenclawConfig,
277
+ writeSanitizedOpenclawConfig,
278
+ };
@@ -12,6 +12,7 @@ const {
12
12
  promoteCloneToTarget,
13
13
  alignHookTransforms,
14
14
  applySecretExtraction,
15
+ canonicalizeConfigEnvRefs,
15
16
  isValidTempDir,
16
17
  } = require("../onboarding/import/import-applier");
17
18
  const { cleanupTempClone } = require("../onboarding/github");
@@ -322,10 +323,17 @@ const registerOnboardingRoutes = ({
322
323
  });
323
324
  envVars = extraction.envVars;
324
325
  }
326
+ const canonicalization = canonicalizeConfigEnvRefs({
327
+ fs,
328
+ baseDir: tempDir,
329
+ configFiles: scan.gatewayConfig.files,
330
+ envVars,
331
+ });
332
+ envVars = canonicalization.envVars;
325
333
 
326
- const configFiles = ["openclaw.json"].filter((f) =>
327
- fs.existsSync(path.join(tempDir, f)),
328
- );
334
+ const configFiles = Array.isArray(scan.gatewayConfig?.files)
335
+ ? scan.gatewayConfig.files
336
+ : ["openclaw.json"].filter((f) => fs.existsSync(path.join(tempDir, f)));
329
337
  const transformAlignment = alignHookTransforms({
330
338
  fs,
331
339
  baseDir: tempDir,
@@ -408,6 +416,7 @@ const registerOnboardingRoutes = ({
408
416
  placeholderReview,
409
417
  sourceLayout: scan.sourceLayout,
410
418
  envVarsImported: envVars.length,
419
+ canonicalizedEnvRefs: canonicalization.rewrittenRefs,
411
420
  transformsAligned: transformAlignment.alignedCount,
412
421
  });
413
422
  } catch (err) {
@@ -1,6 +1,7 @@
1
1
  const registerProxyRoutes = ({
2
2
  app,
3
3
  proxy,
4
+ getGatewayUrl,
4
5
  SETUP_API_PREFIXES,
5
6
  requireAuth,
6
7
  webhookMiddleware,
@@ -13,20 +14,22 @@ const registerProxyRoutes = ({
13
14
 
14
15
  app.all("/openclaw", requireAuth, (req, res) => {
15
16
  req.url = "/";
16
- proxy.web(req, res);
17
+ proxy.web(req, res, { target: getGatewayUrl() });
17
18
  });
18
19
  app.all(kOpenClawPathPattern, requireAuth, (req, res) => {
19
20
  req.url = req.url.replace(/^\/openclaw/, "");
20
- proxy.web(req, res);
21
+ proxy.web(req, res, { target: getGatewayUrl() });
21
22
  });
22
- app.all(kAssetsPathPattern, requireAuth, (req, res) => proxy.web(req, res));
23
+ app.all(kAssetsPathPattern, requireAuth, (req, res) =>
24
+ proxy.web(req, res, { target: getGatewayUrl() }),
25
+ );
23
26
 
24
27
  app.all(kHooksPathPattern, webhookMiddleware);
25
28
  app.all(kWebhookPathPattern, webhookMiddleware);
26
29
 
27
30
  app.all(kApiPathPattern, (req, res) => {
28
31
  if (SETUP_API_PREFIXES.some((p) => req.path.startsWith(p))) return;
29
- proxy.web(req, res);
32
+ proxy.web(req, res, { target: getGatewayUrl() });
30
33
  });
31
34
  };
32
35
 
@@ -111,10 +111,24 @@ const registerSystemRoutes = ({
111
111
  "anthropic",
112
112
  "openai",
113
113
  "google",
114
+ "opencode",
115
+ "openrouter",
116
+ "zai",
117
+ "vercel-ai-gateway",
118
+ "kilocode",
119
+ "xai",
114
120
  "mistral",
121
+ "cerebras",
122
+ "moonshot",
123
+ "kimi-coding",
124
+ "volcengine",
125
+ "byteplus",
126
+ "synthetic",
127
+ "minimax",
115
128
  "voyage",
116
129
  "groq",
117
130
  "deepgram",
131
+ "vllm",
118
132
  ];
119
133
  for (const provider of providers) {
120
134
  const envKey = authProfiles.getEnvVarForApiKeyProvider?.(provider);
@@ -122,11 +122,10 @@ const buildGmailDedupedBodyBuffer = ({ parsedBody, filteredMessages }) => {
122
122
 
123
123
  const createWebhookMiddleware = ({
124
124
  gatewayUrl,
125
+ getGatewayUrl,
125
126
  insertRequest,
126
127
  maxPayloadBytes = 50 * 1024,
127
128
  }) => {
128
- const gateway = new URL(gatewayUrl);
129
- const protocolClient = gateway.protocol === "https:" ? https : http;
130
129
  const gmailSeenMessageIds = new Map();
131
130
  let lastGmailDedupeCleanupAt = 0;
132
131
 
@@ -141,6 +140,10 @@ const createWebhookMiddleware = ({
141
140
  };
142
141
 
143
142
  return (req, res) => {
143
+ const resolvedGatewayUrl =
144
+ typeof getGatewayUrl === "function" ? getGatewayUrl() : gatewayUrl;
145
+ const gateway = new URL(resolvedGatewayUrl);
146
+ const protocolClient = gateway.protocol === "https:" ? https : http;
144
147
  const inboundUrl = new URL(req.url, `http://${req.headers.host || "localhost"}`);
145
148
  let tokenFromQuery = "";
146
149
  if (!req.headers.authorization && inboundUrl.searchParams.has("token")) {
package/lib/server.js CHANGED
@@ -67,6 +67,7 @@ const {
67
67
  } = require("./server/env");
68
68
  const {
69
69
  gatewayEnv,
70
+ getGatewayUrl,
70
71
  isOnboarded,
71
72
  isGatewayRunning,
72
73
  startGateway,
@@ -122,7 +123,7 @@ const { registerUsageRoutes } = require("./server/routes/usage");
122
123
  const { registerGmailRoutes } = require("./server/routes/gmail");
123
124
  const { registerDoctorRoutes } = require("./server/routes/doctor");
124
125
 
125
- const { PORT, GATEWAY_URL, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
126
+ const { PORT, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
126
127
 
127
128
  startEnvWatcher();
128
129
  attachGatewaySignalHandlers();
@@ -139,7 +140,7 @@ app.use("/gmail-pubsub", express.raw({ type: "*/*", limit: "5mb" }));
139
140
  app.use(express.json({ limit: "5mb" }));
140
141
 
141
142
  const proxy = httpProxy.createProxyServer({
142
- target: GATEWAY_URL,
143
+ target: getGatewayUrl(),
143
144
  ws: true,
144
145
  changeOrigin: true,
145
146
  });
@@ -196,7 +197,7 @@ initDoctorDb({
196
197
  rootDir: constants.kRootDir,
197
198
  });
198
199
  const webhookMiddleware = createWebhookMiddleware({
199
- gatewayUrl: constants.GATEWAY_URL,
200
+ getGatewayUrl,
200
201
  insertRequest,
201
202
  maxPayloadBytes: constants.kMaxPayloadBytes,
202
203
  });
@@ -382,6 +383,7 @@ registerDoctorRoutes({
382
383
  registerProxyRoutes({
383
384
  app,
384
385
  proxy,
386
+ getGatewayUrl,
385
387
  SETUP_API_PREFIXES,
386
388
  requireAuth,
387
389
  webhookMiddleware,
@@ -407,7 +409,7 @@ server.on("upgrade", (req, socket, head) => {
407
409
  return;
408
410
  }
409
411
  }
410
- proxy.ws(req, socket, head);
412
+ proxy.ws(req, socket, head, { target: getGatewayUrl() });
411
413
  });
412
414
 
413
415
  server.listen(PORT, "0.0.0.0", () => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.5.1",
3
+ "version": "0.5.3",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -31,7 +31,7 @@
31
31
  "dependencies": {
32
32
  "express": "^4.21.0",
33
33
  "http-proxy": "^1.18.1",
34
- "openclaw": "2026.3.2"
34
+ "openclaw": "2026.3.7"
35
35
  },
36
36
  "devDependencies": {
37
37
  "@vitest/coverage-v8": "^4.0.18",