neoagent 2.2.1-beta.7 → 2.3.0

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.
@@ -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
 
@@ -22,29 +22,17 @@ const {
22
22
  createGenericPlatformClass,
23
23
  } = require('./http_platforms');
24
24
  const { normalizeOutgoingMessageForPlatform } = require('./formatting_guides');
25
-
26
- const GENERIC_ALLOWLIST_PLATFORMS = new Set([
27
- 'slack',
28
- 'google_chat',
29
- 'teams',
30
- 'matrix',
31
- 'signal',
32
- 'imessage',
33
- 'bluebubbles',
34
- 'irc',
35
- 'feishu',
36
- 'line',
37
- 'mattermost',
38
- 'nextcloud_talk',
39
- 'nostr',
40
- 'synology_chat',
41
- 'tlon',
42
- 'twitch',
43
- 'zalo',
44
- 'zalo_personal',
45
- 'wechat',
46
- 'webchat',
47
- ]);
25
+ const {
26
+ accessPolicyKey,
27
+ legacyWhitelistKey,
28
+ getPlatformAccessCapabilities,
29
+ normalizeAccessPolicy,
30
+ migrateLegacyWhitelist,
31
+ parseStoredAccessPolicy,
32
+ evaluateAccessPolicy,
33
+ summarizeAccessPolicy,
34
+ classifyRecentTarget,
35
+ } = require('./access_policy');
48
36
 
49
37
  const LEGACY_WHATSAPP_AUTH_DIR = path.join(DATA_DIR, 'whatsapp-auth');
50
38
 
@@ -70,6 +58,7 @@ class MessagingManager extends EventEmitter {
70
58
  this.io = io;
71
59
  this.voiceRuntimeManager = options.voiceRuntimeManager || null;
72
60
  this.platforms = new Map();
61
+ this.accessSuggestions = new Map();
73
62
  this.messageHandlers = [];
74
63
  this.isShuttingDown = false;
75
64
  this.platformTypes = {
@@ -176,6 +165,41 @@ class MessagingManager extends EventEmitter {
176
165
  .get(userId, key);
177
166
  }
178
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
+
179
203
  _scopedPlatformAuthDir(userId, agentId, platformName) {
180
204
  return path.join(
181
205
  AGENT_DATA_DIR,
@@ -249,6 +273,7 @@ class MessagingManager extends EventEmitter {
249
273
  config = { ...(config || {}) };
250
274
  config.userId = userId;
251
275
  config.agentId = agentId;
276
+ config.accessPolicy = this._loadAccessPolicy(userId, agentId, platformName);
252
277
  const PlatformClass = this.platformTypes[platformName];
253
278
  if (!PlatformClass) throw new Error(`Unknown platform: ${platformName}`);
254
279
 
@@ -268,10 +293,6 @@ class MessagingManager extends EventEmitter {
268
293
  }
269
294
  };
270
295
 
271
- const wlRow = this._setting(userId, agentId, 'platform_whitelist_telnyx');
272
- if (wlRow) {
273
- try { config.allowedNumbers = JSON.parse(wlRow.value); } catch { /* ignore */ }
274
- }
275
296
  const secretRow = this._setting(userId, agentId, 'platform_voice_secret_telnyx');
276
297
  if (secretRow) {
277
298
  try { config.voiceSecret = JSON.parse(secretRow.value); } catch { config.voiceSecret = secretRow.value; }
@@ -301,32 +322,6 @@ class MessagingManager extends EventEmitter {
301
322
  config.voiceRuntimeManager = this.voiceRuntimeManager || null;
302
323
  }
303
324
 
304
- // Inject saved allowlists for platforms that enforce access in the adapter.
305
- if (platformName === 'discord') {
306
- const wlRow = this._setting(userId, agentId, 'platform_whitelist_discord');
307
- if (wlRow) {
308
- try { config.allowedIds = JSON.parse(wlRow.value); } catch { /* ignore */ }
309
- }
310
- }
311
-
312
- // For Telegram, inject saved allowedIds whitelist
313
- if (platformName === 'telegram') {
314
- const wlRow = this._setting(userId, agentId, 'platform_whitelist_telegram');
315
- if (wlRow) {
316
- try { config.allowedIds = JSON.parse(wlRow.value); } catch { /* ignore */ }
317
- }
318
- }
319
-
320
- if (GENERIC_ALLOWLIST_PLATFORMS.has(platformName)) {
321
- const wlRow = this._setting(userId, agentId, `platform_whitelist_${platformName}`);
322
- if (wlRow) {
323
- try {
324
- const parsed = JSON.parse(wlRow.value);
325
- if (Array.isArray(parsed)) config.allowedIds = parsed;
326
- } catch { /* ignore */ }
327
- }
328
- }
329
-
330
325
  const storedConfig = JSON.stringify(this._persistableConfig(config) || {});
331
326
 
332
327
  const key = this._key(userId, agentId, platformName);
@@ -373,22 +368,26 @@ class MessagingManager extends EventEmitter {
373
368
 
374
369
  // Telnyx-specific: blocked inbound caller notification
375
370
  platform.on('blocked_caller', (info) => {
371
+ this._rememberAccessSuggestions(userId, agentId, platformName, info?.suggestions || []);
376
372
  this.io.to(`user:${userId}`).emit('messaging:blocked_sender', {
377
373
  platform: platformName,
378
374
  sender: info.caller,
379
375
  chatId: info.ccId,
380
- senderName: null
376
+ senderName: null,
377
+ meta: info.meta || '',
378
+ suggestions: info.suggestions || null,
381
379
  });
382
380
  });
383
381
 
384
- // Discord / Telegram: blocked sender notification with suggestions
382
+ // Adapter-level blocked sender notification with suggestions
385
383
  platform.on('blocked_sender', (info) => {
384
+ this._rememberAccessSuggestions(userId, agentId, platformName, info?.suggestions || []);
386
385
  this.io.to(`user:${userId}`).emit('messaging:blocked_sender', {
387
386
  platform: platformName,
388
387
  sender: info.sender,
389
388
  chatId: info.chatId,
390
389
  senderName: info.senderName || null,
391
- 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)),
392
391
  suggestions: info.suggestions || null,
393
392
  });
394
393
  });
@@ -679,13 +678,88 @@ class MessagingManager extends EventEmitter {
679
678
  };
680
679
  }
681
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
+
682
757
  /**
683
758
  * Update the allowed-numbers list on a live Telnyx platform instance.
684
759
  */
685
760
  updateTelnyxAllowedNumbers(userId, numbers, options = {}) {
686
- const key = this._key(userId, this._agentId(userId, options), 'telnyx');
687
- const platform = this.platforms.get(key);
688
- if (platform?.setAllowedNumbers) platform.setAllowedNumbers(numbers);
761
+ const migrated = migrateLegacyWhitelist('telnyx', numbers);
762
+ return this.setAccessPolicy(userId, 'telnyx', migrated, options);
689
763
  }
690
764
 
691
765
  /**
@@ -710,10 +784,7 @@ class MessagingManager extends EventEmitter {
710
784
  * Accepts prefixed strings: "user:ID", "guild:ID", "channel:ID"
711
785
  */
712
786
  updateDiscordAllowedIds(userId, ids, options = {}) {
713
- const key = this._key(userId, this._agentId(userId, options), 'discord');
714
- const platform = this.platforms.get(key);
715
- if (platform?.setAllowedEntries) platform.setAllowedEntries(ids);
716
- else if (platform?.setAllowedIds) platform.setAllowedIds(ids); // legacy fallback
787
+ return this.setAccessPolicy(userId, 'discord', migrateLegacyWhitelist('discord', ids), options);
717
788
  }
718
789
 
719
790
  /**
@@ -721,15 +792,11 @@ class MessagingManager extends EventEmitter {
721
792
  * Accepts prefixed strings: "user:ID", "group:ID"
722
793
  */
723
794
  updateTelegramAllowedIds(userId, ids, options = {}) {
724
- const key = this._key(userId, this._agentId(userId, options), 'telegram');
725
- const platform = this.platforms.get(key);
726
- if (platform?.setAllowedEntries) platform.setAllowedEntries(ids);
795
+ return this.setAccessPolicy(userId, 'telegram', migrateLegacyWhitelist('telegram', ids), options);
727
796
  }
728
797
 
729
798
  updateAllowedEntries(userId, platformName, ids, options = {}) {
730
- const key = this._key(userId, this._agentId(userId, options), platformName);
731
- const platform = this.platforms.get(key);
732
- if (platform?.setAllowedEntries) platform.setAllowedEntries(ids);
799
+ return this.setAccessPolicy(userId, platformName, migrateLegacyWhitelist(platformName, ids), options);
733
800
  }
734
801
  }
735
802