@chrysb/alphaclaw 0.8.3 → 0.8.4

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.
@@ -1,7 +1,10 @@
1
- const path = require("path");
2
1
  const crypto = require("crypto");
3
2
  const { parseJsonObjectFromNoisyOutput } = require("../utils/json");
4
3
  const { quoteShellArg } = require("../utils/shell");
4
+ const {
5
+ readExecApprovalsConfig,
6
+ writeExecApprovalsConfig,
7
+ } = require("../exec-defaults-config");
5
8
 
6
9
  const kAllowedExecHosts = new Set(["gateway", "node"]);
7
10
  const kAllowedExecSecurity = new Set(["deny", "allowlist", "full"]);
@@ -81,23 +84,6 @@ const parseNodeBrowserStatus = (stdout) => {
81
84
  return decodedResult && typeof decodedResult === "object" ? decodedResult : null;
82
85
  };
83
86
 
84
- const readExecApprovalsFile = ({ fsModule, openclawDir }) => {
85
- const filePath = path.join(openclawDir, "exec-approvals.json");
86
- try {
87
- const raw = fsModule.readFileSync(filePath, "utf8");
88
- const parsed = JSON.parse(raw);
89
- return parsed && typeof parsed === "object" ? parsed : { version: 1 };
90
- } catch {
91
- return { version: 1 };
92
- }
93
- };
94
-
95
- const writeExecApprovalsFile = ({ fsModule, openclawDir, file }) => {
96
- const filePath = path.join(openclawDir, "exec-approvals.json");
97
- fsModule.mkdirSync(path.dirname(filePath), { recursive: true });
98
- fsModule.writeFileSync(filePath, JSON.stringify(file, null, 2) + "\n", "utf8");
99
- };
100
-
101
87
  const ensureWildcardAgent = (file) => {
102
88
  const agents = file.agents && typeof file.agents === "object" ? file.agents : {};
103
89
  const wildcard =
@@ -372,7 +358,7 @@ const registerNodeRoutes = ({
372
358
 
373
359
  app.get("/api/nodes/exec-approvals", (_req, res) => {
374
360
  const approvals = ensureWildcardAgent(
375
- readExecApprovalsFile({ fsModule, openclawDir }),
361
+ readExecApprovalsConfig({ fsModule, openclawDir }),
376
362
  );
377
363
  const allowlist = approvals?.agents?.["*"]?.allowlist || [];
378
364
  return res.json({
@@ -388,7 +374,7 @@ const registerNodeRoutes = ({
388
374
  return res.status(400).json({ ok: false, error: "pattern is required" });
389
375
  }
390
376
  const approvals = ensureWildcardAgent(
391
- readExecApprovalsFile({ fsModule, openclawDir }),
377
+ readExecApprovalsConfig({ fsModule, openclawDir }),
392
378
  );
393
379
  const allowlist = approvals.agents["*"].allowlist;
394
380
  const existing = allowlist.find(
@@ -403,7 +389,7 @@ const registerNodeRoutes = ({
403
389
  lastUsedAt: Date.now(),
404
390
  };
405
391
  approvals.agents["*"].allowlist = [...allowlist, entry];
406
- writeExecApprovalsFile({ fsModule, openclawDir, file: approvals });
392
+ writeExecApprovalsConfig({ fsModule, openclawDir, file: approvals });
407
393
  return res.json({ ok: true, entry });
408
394
  });
409
395
 
@@ -413,7 +399,7 @@ const registerNodeRoutes = ({
413
399
  return res.status(400).json({ ok: false, error: "id is required" });
414
400
  }
415
401
  const approvals = ensureWildcardAgent(
416
- readExecApprovalsFile({ fsModule, openclawDir }),
402
+ readExecApprovalsConfig({ fsModule, openclawDir }),
417
403
  );
418
404
  const allowlist = approvals.agents["*"].allowlist;
419
405
  const nextAllowlist = allowlist.filter((entry) => String(entry?.id || "") !== id);
@@ -421,7 +407,7 @@ const registerNodeRoutes = ({
421
407
  return res.status(404).json({ ok: false, error: "Allowlist entry not found" });
422
408
  }
423
409
  approvals.agents["*"].allowlist = nextAllowlist;
424
- writeExecApprovalsFile({ fsModule, openclawDir, file: approvals });
410
+ writeExecApprovalsConfig({ fsModule, openclawDir, file: approvals });
425
411
  return res.json({ ok: true });
426
412
  });
427
413
  };
@@ -1,4 +1,5 @@
1
1
  const runOnboardedBootSequence = ({
2
+ ensureManagedExecDefaults,
2
3
  ensureUsageTrackerPluginConfig,
3
4
  doSyncPromptFiles,
4
5
  reloadEnv,
@@ -10,6 +11,13 @@ const runOnboardedBootSequence = ({
10
11
  watchdog,
11
12
  gmailWatchService,
12
13
  }) => {
14
+ try {
15
+ ensureManagedExecDefaults();
16
+ } catch (error) {
17
+ console.error(
18
+ `[alphaclaw] Failed to ensure managed exec defaults on boot: ${error.message}`,
19
+ );
20
+ }
13
21
  try {
14
22
  ensureUsageTrackerPluginConfig();
15
23
  } catch (error) {
@@ -1,35 +1,95 @@
1
1
  const fs = require("fs");
2
2
  const path = require("path");
3
3
  const { OPENCLAW_DIR } = require("./constants");
4
+ const { createSlackApi } = require("./slack-api");
4
5
 
5
- const getPairedIds = (channel) => {
6
+ const kSlackBotEnvKey = "SLACK_BOT_TOKEN";
7
+
8
+ const normalizeAccountId = (value) =>
9
+ String(value || "").trim().toLowerCase() || "default";
10
+
11
+ const resolveCredentialPairingAccountId = ({ channel, fileName }) => {
12
+ const prefix = `${String(channel || "").trim().toLowerCase()}-`;
13
+ const suffix = "-allowFrom.json";
14
+ const rawFileName = String(fileName || "").trim();
15
+ if (!rawFileName.startsWith(prefix) || !rawFileName.endsWith(suffix)) {
16
+ return "";
17
+ }
18
+ return normalizeAccountId(rawFileName.slice(prefix.length, -suffix.length));
19
+ };
20
+
21
+ const deriveSlackBotEnvKey = (accountId = "default") => {
22
+ const normalizedAccountId = normalizeAccountId(accountId);
23
+ if (normalizedAccountId === "default") return kSlackBotEnvKey;
24
+ return `${kSlackBotEnvKey}_${normalizedAccountId.replace(/-/g, "_").toUpperCase()}`;
25
+ };
26
+
27
+ const getPairedTargetsByAccount = ({
28
+ channel,
29
+ fsImpl = fs,
30
+ openclawDir = OPENCLAW_DIR,
31
+ }) => {
6
32
  const safeChannel = String(channel || "").trim().toLowerCase();
7
- if (!safeChannel) return [];
8
- const credentialsDir = path.join(OPENCLAW_DIR, "credentials");
9
- if (!fs.existsSync(credentialsDir)) return [];
10
- const ids = new Set();
33
+ if (!safeChannel) return new Map();
34
+ const credentialsDir = path.join(openclawDir, "credentials");
35
+ if (!fsImpl.existsSync(credentialsDir)) return new Map();
36
+ const idsByAccount = new Map();
11
37
  try {
12
- const files = fs
38
+ const files = fsImpl
13
39
  .readdirSync(credentialsDir)
14
40
  .filter(
15
41
  (fileName) =>
16
42
  fileName.startsWith(`${safeChannel}-`) && fileName.endsWith("-allowFrom.json"),
17
43
  );
18
44
  for (const fileName of files) {
45
+ const accountId = resolveCredentialPairingAccountId({
46
+ channel: safeChannel,
47
+ fileName,
48
+ });
49
+ if (!accountId) continue;
19
50
  const filePath = path.join(credentialsDir, fileName);
20
- const raw = fs.readFileSync(filePath, "utf8");
51
+ const raw = fsImpl.readFileSync(filePath, "utf8");
21
52
  const parsed = JSON.parse(raw);
22
53
  const allowFrom = Array.isArray(parsed?.allowFrom) ? parsed.allowFrom : [];
54
+ const ids =
55
+ idsByAccount.get(accountId) instanceof Set
56
+ ? idsByAccount.get(accountId)
57
+ : new Set();
23
58
  for (const id of allowFrom) {
24
59
  if (id == null) continue;
25
60
  const value = String(id).trim();
26
61
  if (!value) continue;
27
62
  ids.add(value);
28
63
  }
64
+ idsByAccount.set(accountId, ids);
29
65
  }
30
66
  } catch (err) {
31
67
  console.error(`[watchdog] could not resolve ${safeChannel} allowFrom IDs: ${err.message}`);
32
68
  }
69
+ return new Map(
70
+ Array.from(idsByAccount.entries()).map(([accountId, ids]) => [
71
+ accountId,
72
+ Array.from(ids),
73
+ ]),
74
+ );
75
+ };
76
+
77
+ const getPairedIds = ({
78
+ channel,
79
+ fsImpl = fs,
80
+ openclawDir = OPENCLAW_DIR,
81
+ }) => {
82
+ const ids = new Set();
83
+ const idsByAccount = getPairedTargetsByAccount({
84
+ channel,
85
+ fsImpl,
86
+ openclawDir,
87
+ });
88
+ for (const accountIds of idsByAccount.values()) {
89
+ for (const id of accountIds) {
90
+ ids.add(id);
91
+ }
92
+ }
33
93
  return Array.from(ids);
34
94
  };
35
95
 
@@ -38,18 +98,30 @@ const formatDiscordMessage = (message) =>
38
98
 
39
99
  /**
40
100
  * Track thread state for Slack notifications
41
- * Key: userId, Value: { threadTs, lastEvent }
101
+ * Key: accountId:userId, Value: { threadTs, lastEvent }
42
102
  */
43
103
  const slackThreads = new Map();
44
104
 
45
- const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
105
+ const createWatchdogNotifier = ({
106
+ telegramApi,
107
+ discordApi,
108
+ slackApi,
109
+ readEnvFile = () => [],
110
+ createSlackApi: createSlackApiFactory = createSlackApi,
111
+ fsImpl = fs,
112
+ openclawDir = OPENCLAW_DIR,
113
+ }) => {
46
114
  const notify = async (message, opts = {}) => {
47
115
  const summary = {
48
116
  telegram: { sent: 0, failed: 0, skipped: false, targets: 0 },
49
117
  discord: { sent: 0, failed: 0, skipped: false, targets: 0 },
50
118
  slack: { sent: 0, failed: 0, skipped: false, targets: 0 },
51
119
  };
52
- const telegramTargets = getPairedIds("telegram");
120
+ const telegramTargets = getPairedIds({
121
+ channel: "telegram",
122
+ fsImpl,
123
+ openclawDir,
124
+ });
53
125
  summary.telegram.targets = telegramTargets.length;
54
126
  if (!telegramApi?.sendMessage || !process.env.TELEGRAM_BOT_TOKEN || telegramTargets.length === 0) {
55
127
  summary.telegram.skipped = true;
@@ -67,7 +139,11 @@ const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
67
139
  }
68
140
  }
69
141
 
70
- const discordTargets = getPairedIds("discord");
142
+ const discordTargets = getPairedIds({
143
+ channel: "discord",
144
+ fsImpl,
145
+ openclawDir,
146
+ });
71
147
  summary.discord.targets = discordTargets.length;
72
148
  if (!discordApi?.sendDirectMessage || !process.env.DISCORD_BOT_TOKEN || discordTargets.length === 0) {
73
149
  summary.discord.skipped = true;
@@ -85,61 +161,102 @@ const createWatchdogNotifier = ({ telegramApi, discordApi, slackApi }) => {
85
161
  }
86
162
 
87
163
  // Enhanced Slack notifications with threading and reactions
88
- const slackTargets = getPairedIds("slack");
89
- summary.slack.targets = slackTargets.length;
90
- if (!slackApi?.postMessage || !process.env.SLACK_BOT_TOKEN || slackTargets.length === 0) {
164
+ const slackTargetsByAccount = getPairedTargetsByAccount({
165
+ channel: "slack",
166
+ fsImpl,
167
+ openclawDir,
168
+ });
169
+ summary.slack.targets = Array.from(slackTargetsByAccount.values()).reduce(
170
+ (total, targets) => total + targets.length,
171
+ 0,
172
+ );
173
+ if (summary.slack.targets === 0) {
91
174
  summary.slack.skipped = true;
92
175
  } else {
93
176
  const eventType = opts.eventType || "info"; // crash, recovery, health, info
94
-
95
- for (const userId of slackTargets) {
96
- try {
97
- let threadTs = null;
98
- let shouldCreateNewThread = true;
99
-
100
- // Check if we have an active thread for this user
101
- const existingThread = slackThreads.get(userId);
102
- if (existingThread && existingThread.lastEvent === "crash" && eventType === "recovery") {
103
- // Recovery message goes in the crash thread
104
- threadTs = existingThread.threadTs;
105
- shouldCreateNewThread = false;
177
+ const envVars = typeof readEnvFile === "function" ? readEnvFile() : [];
178
+
179
+ const envMap = new Map(
180
+ (Array.isArray(envVars) ? envVars : [])
181
+ .map((entry) => [
182
+ String(entry?.key || "").trim(),
183
+ String(entry?.value || "").trim(),
184
+ ])
185
+ .filter(([key]) => key),
186
+ );
187
+
188
+ for (const [accountId, slackTargets] of slackTargetsByAccount.entries()) {
189
+ if (!slackTargets.length) continue;
190
+ const envKey = deriveSlackBotEnvKey(accountId);
191
+ const botToken = String(envMap.get(envKey) || process.env[envKey] || "").trim();
192
+ if (!botToken) {
193
+ summary.slack.failed += slackTargets.length;
194
+ for (const userId of slackTargets) {
195
+ console.error(
196
+ `[watchdog] slack notification failed for ${accountId}/${userId}: missing ${envKey}`,
197
+ );
106
198
  }
199
+ continue;
200
+ }
107
201
 
108
- // Send message (in thread if continuing conversation)
109
- const result = await slackApi.postMessage(userId, String(message || ""), {
110
- thread_ts: threadTs,
111
- mrkdwn: true,
112
- });
202
+ const accountSlackApi =
203
+ accountId === "default" &&
204
+ slackApi?.postMessage &&
205
+ botToken === String(process.env.SLACK_BOT_TOKEN || "").trim()
206
+ ? slackApi
207
+ : createSlackApiFactory(() => botToken);
208
+
209
+ for (const userId of slackTargets) {
210
+ try {
211
+ let threadTs = null;
212
+ let shouldCreateNewThread = true;
213
+ const threadKey = `${accountId}:${userId}`;
214
+
215
+ const existingThread = slackThreads.get(threadKey);
216
+ if (existingThread && existingThread.lastEvent === "crash" && eventType === "recovery") {
217
+ threadTs = existingThread.threadTs;
218
+ shouldCreateNewThread = false;
219
+ }
113
220
 
114
- // Store thread for future related messages
115
- if (shouldCreateNewThread && result.ts) {
116
- slackThreads.set(userId, {
117
- threadTs: result.ts,
118
- lastEvent: eventType,
221
+ const result = await accountSlackApi.postMessage(userId, String(message || ""), {
222
+ thread_ts: threadTs,
223
+ mrkdwn: true,
119
224
  });
120
- }
121
225
 
122
- // Add reactions based on event type
123
- // Use result.channel (the actual conversation/DM ID) instead of userId
124
- if (result.ts && result.channel && slackApi.addReaction) {
125
- try {
126
- if (eventType === "crash") {
127
- await slackApi.addReaction(result.channel, result.ts, "x");
128
- } else if (eventType === "recovery") {
129
- await slackApi.addReaction(result.channel, result.ts, "white_check_mark");
130
- } else if (eventType === "health") {
131
- await slackApi.addReaction(result.channel, result.ts, "heart");
226
+ if (shouldCreateNewThread && result.ts) {
227
+ slackThreads.set(threadKey, {
228
+ threadTs: result.ts,
229
+ lastEvent: eventType,
230
+ });
231
+ }
232
+
233
+ if (result.ts && result.channel && accountSlackApi.addReaction) {
234
+ try {
235
+ if (eventType === "crash") {
236
+ await accountSlackApi.addReaction(result.channel, result.ts, "x");
237
+ } else if (eventType === "recovery") {
238
+ await accountSlackApi.addReaction(
239
+ result.channel,
240
+ result.ts,
241
+ "white_check_mark",
242
+ );
243
+ } else if (eventType === "health") {
244
+ await accountSlackApi.addReaction(result.channel, result.ts, "heart");
245
+ }
246
+ } catch (reactionErr) {
247
+ console.error(
248
+ `[watchdog] slack reaction failed for ${accountId}/${userId}: ${reactionErr.message}`,
249
+ );
132
250
  }
133
- } catch (reactionErr) {
134
- // Reactions are nice-to-have, don't fail the whole notification
135
- console.error(`[watchdog] slack reaction failed for ${userId}: ${reactionErr.message}`);
136
251
  }
137
- }
138
252
 
139
- summary.slack.sent += 1;
140
- } catch (err) {
141
- summary.slack.failed += 1;
142
- console.error(`[watchdog] slack notification failed for ${userId}: ${err.message}`);
253
+ summary.slack.sent += 1;
254
+ } catch (err) {
255
+ summary.slack.failed += 1;
256
+ console.error(
257
+ `[watchdog] slack notification failed for ${accountId}/${userId}: ${err.message}`,
258
+ );
259
+ }
143
260
  }
144
261
  }
145
262
  }
package/lib/server.js CHANGED
@@ -138,6 +138,9 @@ const {
138
138
  const {
139
139
  ensureUsageTrackerPluginConfig,
140
140
  } = require("./server/usage-tracker-config");
141
+ const {
142
+ ensureManagedExecDefaults,
143
+ } = require("./server/exec-defaults-config");
141
144
 
142
145
  const { PORT, kTrustProxyHops, SETUP_API_PREFIXES } = constants;
143
146
 
@@ -223,7 +226,12 @@ const webhookMiddleware = createWebhookMiddleware({
223
226
  const telegramApi = createTelegramApi(() => process.env.TELEGRAM_BOT_TOKEN);
224
227
  const discordApi = createDiscordApi(() => process.env.DISCORD_BOT_TOKEN);
225
228
  const slackApi = createSlackApi(() => process.env.SLACK_BOT_TOKEN);
226
- const watchdogNotifier = createWatchdogNotifier({ telegramApi, discordApi, slackApi });
229
+ const watchdogNotifier = createWatchdogNotifier({
230
+ telegramApi,
231
+ discordApi,
232
+ slackApi,
233
+ readEnvFile,
234
+ });
227
235
  const watchdog = createWatchdog({
228
236
  clawCmd,
229
237
  launchGatewayProcess,
@@ -379,6 +387,11 @@ startServerLifecycle({
379
387
  PORT,
380
388
  isOnboarded,
381
389
  runOnboardedBootSequence,
390
+ ensureManagedExecDefaults: () =>
391
+ ensureManagedExecDefaults({
392
+ fsModule: fs,
393
+ openclawDir: constants.OPENCLAW_DIR,
394
+ }),
382
395
  ensureUsageTrackerPluginConfig: () =>
383
396
  ensureUsageTrackerPluginConfig({
384
397
  fsModule: fs,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chrysb/alphaclaw",
3
- "version": "0.8.3",
3
+ "version": "0.8.4",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -36,7 +36,7 @@
36
36
  "dependencies": {
37
37
  "express": "^4.21.0",
38
38
  "http-proxy": "^1.18.1",
39
- "openclaw": "2026.3.28",
39
+ "openclaw": "2026.4.1",
40
40
  "patch-package": "^8.0.1",
41
41
  "ws": "^8.19.0"
42
42
  },
@@ -0,0 +1,13 @@
1
+ diff --git a/node_modules/openclaw/dist/gateway-cli-6Ksv5U_O.js b/node_modules/openclaw/dist/gateway-cli-6Ksv5U_O.js
2
+ index 4c742cee..bb87239b 100644
3
+ --- a/node_modules/openclaw/dist/gateway-cli-6Ksv5U_O.js
4
+ +++ b/node_modules/openclaw/dist/gateway-cli-6Ksv5U_O.js
5
+ @@ -26669,7 +26669,7 @@ function attachGatewayWsMessageHandler(params) {
6
+ close(1008, truncateCloseReason(authMessage));
7
+ };
8
+ const clearUnboundScopes = () => {
9
+ - if (scopes.length > 0) {
10
+ + if (scopes.length > 0 && !sharedAuthOk) {
11
+ scopes = [];
12
+ connectParams.scopes = scopes;
13
+ }
@@ -1,13 +0,0 @@
1
- diff --git a/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js b/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
2
- index ca48b932..c12478c4 100644
3
- --- a/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
4
- +++ b/node_modules/openclaw/dist/gateway-cli-DlnlX7IW.js
5
- @@ -25935,7 +25935,7 @@ function attachGatewayWsMessageHandler(params) {
6
- close(1008, truncateCloseReason(authMessage));
7
- };
8
- const clearUnboundScopes = () => {
9
- - if (scopes.length > 0) {
10
- + if (scopes.length > 0 && !sharedAuthOk) {
11
- scopes = [];
12
- connectParams.scopes = scopes;
13
- }