neoagent 2.2.1-beta.6 → 2.2.1-beta.8

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.
Files changed (68) hide show
  1. package/docs/automation.md +2 -2
  2. package/docs/capabilities.md +7 -10
  3. package/docs/hardware.md +4 -7
  4. package/docs/index.md +7 -7
  5. package/docs/integrations.md +1 -1
  6. package/docs/migration.md +238 -0
  7. package/docs/operations.md +1 -1
  8. package/docs/why-neoagent.md +2 -2
  9. package/lib/manager.js +99 -2
  10. package/lib/migrations.js +409 -0
  11. package/package.json +1 -1
  12. package/server/catalog_sources/store-bundles/skills/productivity/migration/SKILL.md +173 -0
  13. package/server/db/database.js +76 -61
  14. package/server/http/routes.js +1 -2
  15. package/server/public/assets/AssetManifest.json +1 -1
  16. package/server/public/assets/fonts/MaterialIcons-Regular.otf +0 -0
  17. package/server/public/flutter_bootstrap.js +1 -1
  18. package/server/public/main.dart.js +75830 -74189
  19. package/server/routes/auth.js +13 -5
  20. package/server/routes/integrations.js +22 -0
  21. package/server/routes/messaging.js +41 -5
  22. package/server/routes/settings.js +1 -0
  23. package/server/routes/{scheduler.js → tasks.js} +31 -29
  24. package/server/routes/widgets.js +7 -7
  25. package/server/services/ai/capabilityHealth.js +4 -4
  26. package/server/services/ai/engine.js +9 -9
  27. package/server/services/ai/systemPrompt.js +7 -7
  28. package/server/services/ai/taskAnalysis.js +3 -3
  29. package/server/services/ai/toolResult.js +6 -8
  30. package/server/services/ai/tools.js +62 -95
  31. package/server/services/commands/router.js +14 -6
  32. package/server/services/integrations/google/provider.js +20 -2
  33. package/server/services/integrations/manager.js +79 -8
  34. package/server/services/integrations/whatsapp/provider.js +23 -1
  35. package/server/services/manager.js +14 -14
  36. package/server/services/memory/manager.js +7 -7
  37. package/server/services/memory/policy.js +1 -1
  38. package/server/services/messaging/access_policy.js +703 -0
  39. package/server/services/messaging/access_policy.test.js +228 -0
  40. package/server/services/messaging/automation.js +32 -95
  41. package/server/services/messaging/base.js +39 -0
  42. package/server/services/messaging/discord.js +61 -46
  43. package/server/services/messaging/formatting_guides.js +0 -4
  44. package/server/services/messaging/http_platforms.js +178 -15
  45. package/server/services/messaging/manager.js +136 -71
  46. package/server/services/messaging/telegram.js +54 -40
  47. package/server/services/messaging/telnyx.js +43 -14
  48. package/server/services/messaging/whatsapp.js +27 -0
  49. package/server/services/tasks/adapters/gmail_message_received.js +36 -0
  50. package/server/services/tasks/adapters/index.js +10 -0
  51. package/server/services/tasks/adapters/outlook_email_received.js +38 -0
  52. package/server/services/tasks/adapters/schedule.js +57 -0
  53. package/server/services/tasks/adapters/slack_message_received.js +39 -0
  54. package/server/services/tasks/adapters/teams_message_received.js +39 -0
  55. package/server/services/tasks/adapters/whatsapp_personal_message_received.js +42 -0
  56. package/server/services/tasks/integration_runtime.js +260 -0
  57. package/server/services/tasks/runtime.js +539 -0
  58. package/server/services/{scheduler/cron_utils.js → tasks/schedule_utils.js} +2 -0
  59. package/server/services/tasks/security.js +60 -0
  60. package/server/services/tasks/task_repository.js +162 -0
  61. package/server/services/tasks/trigger_registry.js +29 -0
  62. package/server/services/tasks/utils.js +45 -0
  63. package/server/services/websocket.js +1 -1
  64. package/server/services/widgets/service.js +37 -25
  65. package/server/routes/wearable_device.js +0 -147
  66. package/server/services/messaging/waveshare_wearable.js +0 -40
  67. package/server/services/scheduler/cron.js +0 -580
  68. package/server/services/wearables/device_auth.js +0 -228
@@ -217,6 +217,27 @@ class ConfigurableHttpPlatform extends BasePlatform {
217
217
  if (!inboundAllowed(this.config, req)) return { handled: false, status: 403, body: 'Forbidden' };
218
218
  const msg = genericMessageFromWebhook(this.name, this.config, req);
219
219
  if (!msg) return { handled: true, status: 202, body: 'ignored' };
220
+ const access = this._checkInboundAccess({
221
+ platform: this.name,
222
+ senderId: String(msg.sender || ''),
223
+ chatId: String(msg.chatId || ''),
224
+ isDirect: !msg.isGroup,
225
+ isShared: Boolean(msg.isGroup),
226
+ groupId: msg.isGroup ? String(msg.chatId || '') : '',
227
+ channelId: msg.isGroup ? String(msg.chatId || '') : '',
228
+ serverId: '',
229
+ roomId: msg.isGroup ? String(msg.chatId || '') : '',
230
+ roleIds: [],
231
+ phoneNumber: '',
232
+ wasMentioned: false,
233
+ }, {
234
+ senderName: msg.senderName || null,
235
+ meta: msg.isGroup ? `Chat: ${msg.chatId}` : '',
236
+ groupLabel: String(msg.chatId || ''),
237
+ channelLabel: String(msg.chatId || ''),
238
+ roomLabel: String(msg.chatId || ''),
239
+ });
240
+ if (!access.allowed) return { handled: true, status: 202, body: 'blocked' };
220
241
  this.emit('message', msg);
221
242
  return { handled: true, status: 200, body: 'OK' };
222
243
  }
@@ -318,7 +339,7 @@ class SlackPlatform extends BasePlatform {
318
339
  ? String(event.text).replace(new RegExp(`<@${this._botUserId}>`, 'g'), '').trim()
319
340
  : String(event.text);
320
341
  if (!content) return { handled: true, status: 202, body: 'ignored' };
321
- this.emit('message', {
342
+ const message = {
322
343
  platform: 'slack',
323
344
  chatId: String(event.channel || 'slack'),
324
345
  sender: String(event.user || event.bot_id || 'slack'),
@@ -332,7 +353,28 @@ class SlackPlatform extends BasePlatform {
332
353
  timestamp: event.event_ts ? new Date(Number(event.event_ts) * 1000).toISOString() : new Date().toISOString(),
333
354
  threadTs: event.thread_ts || null,
334
355
  rawMessage: body,
356
+ wasMentioned,
357
+ };
358
+ const access = this._checkInboundAccess({
359
+ platform: 'slack',
360
+ senderId: message.sender,
361
+ chatId: message.chatId,
362
+ isDirect: !isGroup,
363
+ isShared: isGroup,
364
+ groupId: isGroup ? message.chatId : '',
365
+ channelId: isGroup ? message.chatId : '',
366
+ serverId: '',
367
+ roomId: '',
368
+ roleIds: [],
369
+ phoneNumber: '',
370
+ wasMentioned,
371
+ }, {
372
+ senderName: message.senderName,
373
+ meta: isGroup ? `Channel: ${message.chatId}` : '',
374
+ channelLabel: message.chatId,
335
375
  });
376
+ if (!access.allowed) return { handled: true, status: 202, body: 'blocked' };
377
+ this.emit('message', message);
336
378
  return { handled: true, status: 200, body: 'OK' };
337
379
  }
338
380
  }
@@ -345,23 +387,43 @@ class GoogleChatPlatform extends ConfigurableHttpPlatform {
345
387
  async handleWebhook(req) {
346
388
  if (!inboundAllowed(this.config, req)) return { handled: false, status: 403, body: 'Forbidden' };
347
389
  const body = req.body || {};
348
- const message = body.message || body;
349
- const content = message.argumentText || message.text || body.text;
390
+ const incoming = body.message || body;
391
+ const content = incoming.argumentText || incoming.text || body.text;
350
392
  if (!content) return { handled: true, status: 202, body: 'ignored' };
351
- this.emit('message', {
393
+ const message = {
352
394
  platform: 'google_chat',
353
- chatId: String(message.space?.name || body.space?.name || this.config.defaultTo || 'google_chat'),
354
- sender: String(body.user?.name || message.sender?.name || 'google_chat'),
355
- senderName: body.user?.displayName || message.sender?.displayName || null,
356
- senderDisplayName: body.user?.displayName || message.sender?.displayName || null,
357
- senderTag: body.user?.name || message.sender?.name || null,
395
+ chatId: String(incoming.space?.name || body.space?.name || this.config.defaultTo || 'google_chat'),
396
+ sender: String(body.user?.name || incoming.sender?.name || 'google_chat'),
397
+ senderName: body.user?.displayName || incoming.sender?.displayName || null,
398
+ senderDisplayName: body.user?.displayName || incoming.sender?.displayName || null,
399
+ senderTag: body.user?.name || incoming.sender?.name || null,
358
400
  content: String(content),
359
401
  mediaType: null,
360
402
  isGroup: true,
361
- messageId: String(message.name || crypto.randomUUID()),
403
+ messageId: String(incoming.name || crypto.randomUUID()),
362
404
  timestamp: new Date().toISOString(),
363
405
  rawMessage: body,
406
+ };
407
+ const access = this._checkInboundAccess({
408
+ platform: 'google_chat',
409
+ senderId: message.sender,
410
+ chatId: message.chatId,
411
+ isDirect: false,
412
+ isShared: true,
413
+ groupId: '',
414
+ channelId: '',
415
+ serverId: '',
416
+ roomId: message.chatId,
417
+ roleIds: [],
418
+ phoneNumber: '',
419
+ wasMentioned: false,
420
+ }, {
421
+ senderName: message.senderName,
422
+ meta: `Space: ${message.chatId}`,
423
+ roomLabel: message.chatId,
364
424
  });
425
+ if (!access.allowed) return { handled: true, status: 202, body: 'blocked' };
426
+ this.emit('message', message);
365
427
  return { handled: true, status: 200, body: { text: 'Received.' } };
366
428
  }
367
429
  }
@@ -376,7 +438,7 @@ class TeamsPlatform extends ConfigurableHttpPlatform {
376
438
  const body = req.body || {};
377
439
  const content = body.text || body.message?.text || body.value?.text;
378
440
  if (!content) return { handled: true, status: 202, body: 'ignored' };
379
- this.emit('message', {
441
+ const message = {
380
442
  platform: 'teams',
381
443
  chatId: String(body.conversation?.id || body.channelData?.channel?.id || this.config.defaultTo || 'teams'),
382
444
  sender: String(body.from?.id || 'teams'),
@@ -389,7 +451,27 @@ class TeamsPlatform extends ConfigurableHttpPlatform {
389
451
  messageId: String(body.id || crypto.randomUUID()),
390
452
  timestamp: body.timestamp || new Date().toISOString(),
391
453
  rawMessage: body,
454
+ };
455
+ const access = this._checkInboundAccess({
456
+ platform: 'teams',
457
+ senderId: message.sender,
458
+ chatId: message.chatId,
459
+ isDirect: false,
460
+ isShared: true,
461
+ groupId: '',
462
+ channelId: message.chatId,
463
+ serverId: '',
464
+ roomId: '',
465
+ roleIds: [],
466
+ phoneNumber: '',
467
+ wasMentioned: false,
468
+ }, {
469
+ senderName: message.senderName,
470
+ meta: `Conversation: ${message.chatId}`,
471
+ channelLabel: message.chatId,
392
472
  });
473
+ if (!access.allowed) return { handled: true, status: 202, body: 'blocked' };
474
+ this.emit('message', message);
393
475
  return { handled: true, status: 200, body: { type: 'message', text: 'Received.' } };
394
476
  }
395
477
  }
@@ -473,7 +555,7 @@ class MatrixPlatform extends BasePlatform {
473
555
  ? String(content).replaceAll(this.userId, '').trim()
474
556
  : String(content);
475
557
  if (!cleanContent) continue;
476
- this.emit('message', {
558
+ const message = {
477
559
  platform: 'matrix',
478
560
  chatId: roomId,
479
561
  sender: String(event.sender || roomId),
@@ -486,7 +568,27 @@ class MatrixPlatform extends BasePlatform {
486
568
  messageId: String(event.event_id || crypto.randomUUID()),
487
569
  timestamp: event.origin_server_ts ? new Date(event.origin_server_ts).toISOString() : new Date().toISOString(),
488
570
  rawMessage: event,
571
+ };
572
+ const access = this._checkInboundAccess({
573
+ platform: 'matrix',
574
+ senderId: message.sender,
575
+ chatId: message.chatId,
576
+ isDirect: false,
577
+ isShared: true,
578
+ groupId: '',
579
+ channelId: '',
580
+ serverId: '',
581
+ roomId: roomId,
582
+ roleIds: [],
583
+ phoneNumber: '',
584
+ wasMentioned: this.userId ? String(content).includes(this.userId) : false,
585
+ }, {
586
+ senderName: message.senderName,
587
+ meta: `Room: ${roomId}`,
588
+ roomLabel: roomId,
489
589
  });
590
+ if (!access.allowed) continue;
591
+ this.emit('message', message);
490
592
  }
491
593
  }
492
594
  }
@@ -574,7 +676,7 @@ class SignalPlatform extends ConfigurableHttpPlatform {
574
676
  const dataMessage = envelope.dataMessage || {};
575
677
  const content = dataMessage.message || '';
576
678
  if (!content) continue;
577
- this.emit('message', {
679
+ const message = {
578
680
  platform: 'signal',
579
681
  chatId: String(envelope.sourceNumber || envelope.source || 'signal'),
580
682
  sender: String(envelope.sourceNumber || envelope.source || 'signal'),
@@ -587,7 +689,27 @@ class SignalPlatform extends ConfigurableHttpPlatform {
587
689
  messageId: String(envelope.timestamp || crypto.randomUUID()),
588
690
  timestamp: envelope.timestamp ? new Date(Number(envelope.timestamp)).toISOString() : new Date().toISOString(),
589
691
  rawMessage: item,
692
+ };
693
+ const access = this._checkInboundAccess({
694
+ platform: 'signal',
695
+ senderId: message.sender,
696
+ chatId: message.chatId,
697
+ isDirect: !message.isGroup,
698
+ isShared: message.isGroup,
699
+ groupId: message.isGroup ? message.chatId : '',
700
+ channelId: '',
701
+ serverId: '',
702
+ roomId: '',
703
+ roleIds: [],
704
+ phoneNumber: message.sender,
705
+ wasMentioned: false,
706
+ }, {
707
+ senderName: message.senderName,
708
+ meta: message.isGroup ? `Group: ${message.chatId}` : '',
709
+ groupLabel: message.chatId,
590
710
  });
711
+ if (!access.allowed) continue;
712
+ this.emit('message', message);
591
713
  }
592
714
  }
593
715
 
@@ -640,7 +762,7 @@ class LinePlatform extends ConfigurableHttpPlatform {
640
762
  for (const event of events) {
641
763
  const content = event.message?.text || '';
642
764
  if (!content) continue;
643
- this.emit('message', {
765
+ const message = {
644
766
  platform: 'line',
645
767
  chatId: String(event.source?.groupId || event.source?.roomId || event.source?.userId || 'line'),
646
768
  sender: String(event.source?.userId || 'line'),
@@ -652,7 +774,28 @@ class LinePlatform extends ConfigurableHttpPlatform {
652
774
  messageId: String(event.message?.id || event.webhookEventId || crypto.randomUUID()),
653
775
  timestamp: event.timestamp ? new Date(Number(event.timestamp)).toISOString() : new Date().toISOString(),
654
776
  rawMessage: event,
777
+ };
778
+ const access = this._checkInboundAccess({
779
+ platform: 'line',
780
+ senderId: message.sender,
781
+ chatId: message.chatId,
782
+ isDirect: !message.isGroup,
783
+ isShared: message.isGroup,
784
+ groupId: event.source?.groupId ? String(event.source.groupId) : '',
785
+ channelId: '',
786
+ serverId: '',
787
+ roomId: event.source?.roomId ? String(event.source.roomId) : '',
788
+ roleIds: [],
789
+ phoneNumber: '',
790
+ wasMentioned: false,
791
+ }, {
792
+ senderName: message.senderName,
793
+ meta: event.source?.groupId ? `Group: ${event.source.groupId}` : (event.source?.roomId ? `Room: ${event.source.roomId}` : ''),
794
+ groupLabel: event.source?.groupId ? String(event.source.groupId) : '',
795
+ roomLabel: event.source?.roomId ? String(event.source.roomId) : '',
655
796
  });
797
+ if (!access.allowed) continue;
798
+ this.emit('message', message);
656
799
  }
657
800
  return { handled: true, status: 200, body: 'OK' };
658
801
  }
@@ -795,7 +938,7 @@ class IrcPlatform extends BasePlatform {
795
938
  ? content.replace(new RegExp(`(^|\\s)${this.nick.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[:,]?\\s*`, 'i'), ' ').trim()
796
939
  : content;
797
940
  if (!cleanContent) continue;
798
- this.emit('message', {
941
+ const message = {
799
942
  platform: this.name,
800
943
  chatId: target,
801
944
  sender: nick,
@@ -807,7 +950,27 @@ class IrcPlatform extends BasePlatform {
807
950
  isGroup,
808
951
  messageId: crypto.randomUUID(),
809
952
  timestamp: new Date().toISOString(),
953
+ };
954
+ const access = this._checkInboundAccess({
955
+ platform: this.name,
956
+ senderId: nick,
957
+ chatId: target,
958
+ isDirect: !isGroup,
959
+ isShared: isGroup,
960
+ groupId: '',
961
+ channelId: isGroup ? target : '',
962
+ serverId: '',
963
+ roomId: '',
964
+ roleIds: [],
965
+ phoneNumber: '',
966
+ wasMentioned: !isGroup || new RegExp(`(^|\\s)${this.nick.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\$&')}[:,]?\\b`, 'i').test(content),
967
+ }, {
968
+ senderName: nick,
969
+ meta: isGroup ? `Channel: ${target}` : '',
970
+ channelLabel: target,
810
971
  });
972
+ if (!access.allowed) continue;
973
+ this.emit('message', message);
811
974
  }
812
975
  }
813
976
 
@@ -9,7 +9,6 @@ const { WhatsAppPlatform } = require('./whatsapp');
9
9
  const { TelnyxVoicePlatform } = require('./telnyx');
10
10
  const { DiscordPlatform } = require('./discord');
11
11
  const { TelegramPlatform } = require('./telegram');
12
- const { WaveshareWearablePlatform } = require('./waveshare_wearable');
13
12
  const {
14
13
  SlackPlatform,
15
14
  GoogleChatPlatform,
@@ -23,29 +22,17 @@ const {
23
22
  createGenericPlatformClass,
24
23
  } = require('./http_platforms');
25
24
  const { normalizeOutgoingMessageForPlatform } = require('./formatting_guides');
26
-
27
- const GENERIC_ALLOWLIST_PLATFORMS = new Set([
28
- 'slack',
29
- 'google_chat',
30
- 'teams',
31
- 'matrix',
32
- 'signal',
33
- 'imessage',
34
- 'bluebubbles',
35
- 'irc',
36
- 'feishu',
37
- 'line',
38
- 'mattermost',
39
- 'nextcloud_talk',
40
- 'nostr',
41
- 'synology_chat',
42
- 'tlon',
43
- 'twitch',
44
- 'zalo',
45
- 'zalo_personal',
46
- 'wechat',
47
- 'webchat',
48
- ]);
25
+ const {
26
+ accessPolicyKey,
27
+ legacyWhitelistKey,
28
+ getPlatformAccessCapabilities,
29
+ normalizeAccessPolicy,
30
+ migrateLegacyWhitelist,
31
+ parseStoredAccessPolicy,
32
+ evaluateAccessPolicy,
33
+ summarizeAccessPolicy,
34
+ classifyRecentTarget,
35
+ } = require('./access_policy');
49
36
 
50
37
  const LEGACY_WHATSAPP_AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth');
51
38
 
@@ -71,6 +58,7 @@ class MessagingManager extends EventEmitter {
71
58
  this.io = io;
72
59
  this.voiceRuntimeManager = options.voiceRuntimeManager || null;
73
60
  this.platforms = new Map();
61
+ this.accessSuggestions = new Map();
74
62
  this.messageHandlers = [];
75
63
  this.isShuttingDown = false;
76
64
  this.platformTypes = {
@@ -89,7 +77,6 @@ class MessagingManager extends EventEmitter {
89
77
  feishu: createGenericPlatformClass('feishu'),
90
78
  line: LinePlatform,
91
79
  mattermost: MattermostPlatform,
92
- waveshare_wearable: WaveshareWearablePlatform,
93
80
  nextcloud_talk: createGenericPlatformClass('nextcloud_talk'),
94
81
  nostr: createGenericPlatformClass('nostr'),
95
82
  synology_chat: createGenericPlatformClass('synology_chat'),
@@ -178,6 +165,41 @@ class MessagingManager extends EventEmitter {
178
165
  .get(userId, key);
179
166
  }
180
167
 
168
+ _upsertSetting(userId, agentId, key, value) {
169
+ db.prepare(
170
+ `INSERT INTO agent_settings (user_id, agent_id, key, value)
171
+ VALUES (?, ?, ?, ?)
172
+ ON CONFLICT(user_id, agent_id, key) DO UPDATE SET value = excluded.value`
173
+ ).run(userId, agentId, key, JSON.stringify(value));
174
+ }
175
+
176
+ _accessSuggestionKey(userId, agentId, platformName) {
177
+ return `${userId}:${agentId}:${platformName}:access-suggestions`;
178
+ }
179
+
180
+ _rememberAccessSuggestions(userId, agentId, platformName, suggestions = []) {
181
+ if (!Array.isArray(suggestions) || suggestions.length === 0) return;
182
+ const key = this._accessSuggestionKey(userId, agentId, platformName);
183
+ const existing = this.accessSuggestions.get(key) || [];
184
+ const merged = [...suggestions, ...existing].filter((item) => item && item.rule && item.bucket);
185
+ const unique = [];
186
+ const seen = new Set();
187
+ for (const item of merged) {
188
+ const id = `${item.bucket}:${item.rule.scope}:${item.rule.value}`;
189
+ if (seen.has(id)) continue;
190
+ seen.add(id);
191
+ unique.push(item);
192
+ if (unique.length >= 24) break;
193
+ }
194
+ this.accessSuggestions.set(key, unique);
195
+ }
196
+
197
+ _loadAccessPolicy(userId, agentId, platformName) {
198
+ const policyRow = this._setting(userId, agentId, accessPolicyKey(platformName));
199
+ const legacyRow = this._setting(userId, agentId, legacyWhitelistKey(platformName));
200
+ return parseStoredAccessPolicy(platformName, policyRow?.value, legacyRow?.value);
201
+ }
202
+
181
203
  _scopedPlatformAuthDir(userId, agentId, platformName) {
182
204
  return path.join(
183
205
  AGENT_DATA_DIR,
@@ -251,6 +273,7 @@ class MessagingManager extends EventEmitter {
251
273
  config = { ...(config || {}) };
252
274
  config.userId = userId;
253
275
  config.agentId = agentId;
276
+ config.accessPolicy = this._loadAccessPolicy(userId, agentId, platformName);
254
277
  const PlatformClass = this.platformTypes[platformName];
255
278
  if (!PlatformClass) throw new Error(`Unknown platform: ${platformName}`);
256
279
 
@@ -270,10 +293,6 @@ class MessagingManager extends EventEmitter {
270
293
  }
271
294
  };
272
295
 
273
- const wlRow = this._setting(userId, agentId, 'platform_whitelist_telnyx');
274
- if (wlRow) {
275
- try { config.allowedNumbers = JSON.parse(wlRow.value); } catch { /* ignore */ }
276
- }
277
296
  const secretRow = this._setting(userId, agentId, 'platform_voice_secret_telnyx');
278
297
  if (secretRow) {
279
298
  try { config.voiceSecret = JSON.parse(secretRow.value); } catch { config.voiceSecret = secretRow.value; }
@@ -303,32 +322,6 @@ class MessagingManager extends EventEmitter {
303
322
  config.voiceRuntimeManager = this.voiceRuntimeManager || null;
304
323
  }
305
324
 
306
- // Inject saved allowlists for platforms that enforce access in the adapter.
307
- if (platformName === 'discord') {
308
- const wlRow = this._setting(userId, agentId, 'platform_whitelist_discord');
309
- if (wlRow) {
310
- try { config.allowedIds = JSON.parse(wlRow.value); } catch { /* ignore */ }
311
- }
312
- }
313
-
314
- // For Telegram, inject saved allowedIds whitelist
315
- if (platformName === 'telegram') {
316
- const wlRow = this._setting(userId, agentId, 'platform_whitelist_telegram');
317
- if (wlRow) {
318
- try { config.allowedIds = JSON.parse(wlRow.value); } catch { /* ignore */ }
319
- }
320
- }
321
-
322
- if (GENERIC_ALLOWLIST_PLATFORMS.has(platformName)) {
323
- const wlRow = this._setting(userId, agentId, `platform_whitelist_${platformName}`);
324
- if (wlRow) {
325
- try {
326
- const parsed = JSON.parse(wlRow.value);
327
- if (Array.isArray(parsed)) config.allowedIds = parsed;
328
- } catch { /* ignore */ }
329
- }
330
- }
331
-
332
325
  const storedConfig = JSON.stringify(this._persistableConfig(config) || {});
333
326
 
334
327
  const key = this._key(userId, agentId, platformName);
@@ -375,22 +368,26 @@ class MessagingManager extends EventEmitter {
375
368
 
376
369
  // Telnyx-specific: blocked inbound caller notification
377
370
  platform.on('blocked_caller', (info) => {
371
+ this._rememberAccessSuggestions(userId, agentId, platformName, info?.suggestions || []);
378
372
  this.io.to(`user:${userId}`).emit('messaging:blocked_sender', {
379
373
  platform: platformName,
380
374
  sender: info.caller,
381
375
  chatId: info.ccId,
382
- senderName: null
376
+ senderName: null,
377
+ meta: info.meta || '',
378
+ suggestions: info.suggestions || null,
383
379
  });
384
380
  });
385
381
 
386
- // Discord / Telegram: blocked sender notification with suggestions
382
+ // Adapter-level blocked sender notification with suggestions
387
383
  platform.on('blocked_sender', (info) => {
384
+ this._rememberAccessSuggestions(userId, agentId, platformName, info?.suggestions || []);
388
385
  this.io.to(`user:${userId}`).emit('messaging:blocked_sender', {
389
386
  platform: platformName,
390
387
  sender: info.sender,
391
388
  chatId: info.chatId,
392
389
  senderName: info.senderName || null,
393
- meta: info.guildName ? `Server: ${info.guildName}` : (info.groupName ? `Group: ${info.groupName}` : null),
390
+ meta: info.meta || (info.guildName ? `Server: ${info.guildName}` : (info.groupName ? `Group: ${info.groupName}` : null)),
394
391
  suggestions: info.suggestions || null,
395
392
  });
396
393
  });
@@ -681,13 +678,88 @@ class MessagingManager extends EventEmitter {
681
678
  };
682
679
  }
683
680
 
681
+ getAccessPolicy(userId, platformName, options = {}) {
682
+ const agentId = this._agentId(userId, options);
683
+ return this._loadAccessPolicy(userId, agentId, platformName);
684
+ }
685
+
686
+ setAccessPolicy(userId, platformName, policy, options = {}) {
687
+ const agentId = this._agentId(userId, options);
688
+ const normalized = normalizeAccessPolicy(platformName, policy);
689
+ this._upsertSetting(userId, agentId, accessPolicyKey(platformName), normalized);
690
+ const key = this._key(userId, agentId, platformName);
691
+ const platform = this.platforms.get(key);
692
+ if (platform?.setAccessPolicy) {
693
+ platform.setAccessPolicy(normalized);
694
+ }
695
+ return normalized;
696
+ }
697
+
698
+ evaluateAccess(userId, platformName, context, options = {}) {
699
+ const agentId = this._agentId(userId, options);
700
+ const key = this._key(userId, agentId, platformName);
701
+ const platform = this.platforms.get(key);
702
+ if (platform?.evaluateAccess) {
703
+ return platform.evaluateAccess(context);
704
+ }
705
+ return evaluateAccessPolicy(
706
+ this._loadAccessPolicy(userId, agentId, platformName),
707
+ context,
708
+ platformName,
709
+ );
710
+ }
711
+
712
+ async getAccessCatalog(userId, platformName, options = {}) {
713
+ const agentId = this._agentId(userId, options);
714
+ const key = this._key(userId, agentId, platformName);
715
+ const platform = this.platforms.get(key);
716
+ let discoveredTargets = [];
717
+ if (platform?.listAccessTargets) {
718
+ discoveredTargets = await Promise.resolve(platform.listAccessTargets()).catch(() => []);
719
+ }
720
+
721
+ const recentRows = db.prepare(
722
+ `SELECT platform_chat_id, metadata
723
+ FROM messages
724
+ WHERE user_id = ? AND agent_id = ? AND platform = ? AND platform_chat_id IS NOT NULL
725
+ ORDER BY id DESC
726
+ LIMIT 40`
727
+ ).all(userId, agentId, platformName);
728
+ const recentTargets = recentRows
729
+ .map((row) => {
730
+ let metadata = {};
731
+ try {
732
+ metadata = row.metadata ? JSON.parse(row.metadata) : {};
733
+ } catch {
734
+ metadata = {};
735
+ }
736
+ return classifyRecentTarget(platformName, { ...row, metadata });
737
+ })
738
+ .filter(Boolean);
739
+
740
+ const seen = new Set();
741
+ const unique = (items) => items.filter((item) => {
742
+ const keyValue = `${item.bucket}:${item.scope}:${item.value}`;
743
+ if (seen.has(keyValue)) return false;
744
+ seen.add(keyValue);
745
+ return true;
746
+ });
747
+
748
+ return {
749
+ capabilities: getPlatformAccessCapabilities(platformName),
750
+ discoveredTargets: unique([...(Array.isArray(discoveredTargets) ? discoveredTargets : []), ...recentTargets]),
751
+ suggestedTargets: unique(this.accessSuggestions.get(this._accessSuggestionKey(userId, agentId, platformName)) || []),
752
+ policy: this._loadAccessPolicy(userId, agentId, platformName),
753
+ summary: summarizeAccessPolicy(platformName, this._loadAccessPolicy(userId, agentId, platformName)),
754
+ };
755
+ }
756
+
684
757
  /**
685
758
  * Update the allowed-numbers list on a live Telnyx platform instance.
686
759
  */
687
760
  updateTelnyxAllowedNumbers(userId, numbers, options = {}) {
688
- const key = this._key(userId, this._agentId(userId, options), 'telnyx');
689
- const platform = this.platforms.get(key);
690
- if (platform?.setAllowedNumbers) platform.setAllowedNumbers(numbers);
761
+ const migrated = migrateLegacyWhitelist('telnyx', numbers);
762
+ return this.setAccessPolicy(userId, 'telnyx', migrated, options);
691
763
  }
692
764
 
693
765
  /**
@@ -712,10 +784,7 @@ class MessagingManager extends EventEmitter {
712
784
  * Accepts prefixed strings: "user:ID", "guild:ID", "channel:ID"
713
785
  */
714
786
  updateDiscordAllowedIds(userId, ids, options = {}) {
715
- const key = this._key(userId, this._agentId(userId, options), 'discord');
716
- const platform = this.platforms.get(key);
717
- if (platform?.setAllowedEntries) platform.setAllowedEntries(ids);
718
- else if (platform?.setAllowedIds) platform.setAllowedIds(ids); // legacy fallback
787
+ return this.setAccessPolicy(userId, 'discord', migrateLegacyWhitelist('discord', ids), options);
719
788
  }
720
789
 
721
790
  /**
@@ -723,15 +792,11 @@ class MessagingManager extends EventEmitter {
723
792
  * Accepts prefixed strings: "user:ID", "group:ID"
724
793
  */
725
794
  updateTelegramAllowedIds(userId, ids, options = {}) {
726
- const key = this._key(userId, this._agentId(userId, options), 'telegram');
727
- const platform = this.platforms.get(key);
728
- if (platform?.setAllowedEntries) platform.setAllowedEntries(ids);
795
+ return this.setAccessPolicy(userId, 'telegram', migrateLegacyWhitelist('telegram', ids), options);
729
796
  }
730
797
 
731
798
  updateAllowedEntries(userId, platformName, ids, options = {}) {
732
- const key = this._key(userId, this._agentId(userId, options), platformName);
733
- const platform = this.platforms.get(key);
734
- if (platform?.setAllowedEntries) platform.setAllowedEntries(ids);
799
+ return this.setAccessPolicy(userId, platformName, migrateLegacyWhitelist(platformName, ids), options);
735
800
  }
736
801
  }
737
802