neoagent 3.2.1-beta.12 → 3.2.1-beta.13

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.
@@ -102,6 +102,20 @@ async function shouldEngage(ctx) {
102
102
  };
103
103
  }
104
104
 
105
+ if (msg.wasMentioned || msg.repliedToAgent) {
106
+ return {
107
+ decision: 'speak',
108
+ needScore: 1,
109
+ confidence: 1,
110
+ reasonCodes: [msg.repliedToAgent ? 'reply_to_agent' : 'addressed'],
111
+ urgency: 'medium',
112
+ rationale: 'Platform metadata directly addresses the agent.',
113
+ tokenPath: 'gate_skip',
114
+ latencyMs: Date.now() - startedAt,
115
+ turnEpoch,
116
+ };
117
+ }
118
+
105
119
  const mode = config.participationMode || 'automatic';
106
120
  if (mode === 'always') {
107
121
  return {
@@ -116,7 +130,7 @@ async function shouldEngage(ctx) {
116
130
  turnEpoch,
117
131
  };
118
132
  }
119
- if (mode === 'mention_only' && !msg.wasMentioned && !msg.repliedToAgent) {
133
+ if (mode === 'mention_only') {
120
134
  return {
121
135
  decision: 'stay_silent',
122
136
  needScore: 0,
@@ -129,19 +143,6 @@ async function shouldEngage(ctx) {
129
143
  turnEpoch,
130
144
  };
131
145
  }
132
- if (mode === 'mention_only') {
133
- return {
134
- decision: 'speak',
135
- needScore: 1,
136
- confidence: 1,
137
- reasonCodes: [msg.repliedToAgent ? 'reply_to_agent' : 'addressed'],
138
- urgency: 'medium',
139
- rationale: 'The room is mention-only and platform metadata directly addressed the agent.',
140
- tokenPath: 'gate_skip',
141
- latencyMs: Date.now() - startedAt,
142
- turnEpoch,
143
- };
144
- }
145
146
 
146
147
  const threadState = getThreadState(userId, agentId, msg.platform, msg.chatId);
147
148
  const roomMessages = loadRecentRoomMessages({
@@ -25,13 +25,6 @@ function createBehaviorPipeline(deps = {}) {
25
25
  chatId: msg.chatId,
26
26
  isGroup: Boolean(msg.isGroup),
27
27
  });
28
- if (
29
- msg.isGroup
30
- && config.participationModeSource === 'default'
31
- && msg.accessPolicyRequireMention === true
32
- ) {
33
- config.participationMode = 'mention_only';
34
- }
35
28
  return config;
36
29
  }
37
30
 
@@ -65,6 +58,30 @@ function createBehaviorPipeline(deps = {}) {
65
58
  const config = effectiveConfig(userId, agentId, msg);
66
59
  const turnEpoch = Number(msg.behaviorTurnEpoch)
67
60
  || noteInbound({ userId, agentId, msg });
61
+ if (
62
+ msg.isGroup
63
+ && msg.accessPolicyAllowUntagged === false
64
+ && !msg.wasMentioned
65
+ && !msg.repliedToAgent
66
+ ) {
67
+ return {
68
+ engage: false,
69
+ decision: {
70
+ decision: 'stay_silent',
71
+ needScore: 0,
72
+ confidence: 1,
73
+ reasonCodes: ['untagged_disabled_for_shared_space'],
74
+ urgency: 'low',
75
+ rationale: 'Untagged responses are disabled for this shared space.',
76
+ tokenPath: 'gate_skip',
77
+ latencyMs: 0,
78
+ turnEpoch,
79
+ },
80
+ config,
81
+ promptBlocks: [],
82
+ observeResult: null,
83
+ };
84
+ }
68
85
  if (config.enabled === false) {
69
86
  return {
70
87
  engage: true,
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const ACCESS_MODES = Object.freeze(['allowlist', 'open', 'disabled']);
4
+ const ACCESS_POLICY_SCHEMA_VERSION = 3;
4
5
  const ACCESS_MODE_SET = new Set(ACCESS_MODES);
5
6
  const RULE_SCOPES = Object.freeze([
6
7
  'user',
@@ -44,6 +45,7 @@ function capabilityTemplate(overrides = {}) {
44
45
  supportsDirectPolicy: true,
45
46
  supportsSharedPolicy: true,
46
47
  supportsMentionGate: false,
48
+ supportsUntaggedGroupToggle: true,
47
49
  supportsDiscovery: false,
48
50
  directRuleScopes: DIRECT_RULE_SCOPES,
49
51
  sharedSpaceRuleScopes: SHARED_SPACE_RULE_SCOPES,
@@ -55,7 +57,7 @@ function capabilityTemplate(overrides = {}) {
55
57
 
56
58
  const PLATFORM_CAPABILITIES = Object.freeze({
57
59
  whatsapp: capabilityTemplate({
58
- supportsMentionGate: false,
60
+ supportsMentionGate: true,
59
61
  supportsDiscovery: true,
60
62
  directRuleScopes: Object.freeze(['phone_number', 'user', 'chat']),
61
63
  sharedSpaceRuleScopes: Object.freeze(['group', 'chat']),
@@ -65,6 +67,7 @@ const PLATFORM_CAPABILITIES = Object.freeze({
65
67
  telnyx: capabilityTemplate({
66
68
  supportsSharedPolicy: false,
67
69
  supportsMentionGate: false,
70
+ supportsUntaggedGroupToggle: false,
68
71
  supportsDiscovery: false,
69
72
  directRuleScopes: Object.freeze(['phone_number']),
70
73
  sharedSpaceRuleScopes: Object.freeze([]),
@@ -96,14 +99,14 @@ const PLATFORM_CAPABILITIES = Object.freeze({
96
99
  manualEntryHint: 'Add a Slack user or channel id.',
97
100
  }),
98
101
  google_chat: capabilityTemplate({
99
- supportsMentionGate: false,
102
+ supportsMentionGate: true,
100
103
  supportsDiscovery: false,
101
104
  directRuleScopes: Object.freeze(['user', 'chat']),
102
105
  sharedSpaceRuleScopes: Object.freeze(['room', 'chat']),
103
106
  sharedActorRuleScopes: Object.freeze(['user']),
104
107
  }),
105
108
  teams: capabilityTemplate({
106
- supportsMentionGate: false,
109
+ supportsMentionGate: true,
107
110
  supportsDiscovery: false,
108
111
  directRuleScopes: Object.freeze(['user', 'chat']),
109
112
  sharedSpaceRuleScopes: Object.freeze(['channel', 'chat']),
@@ -117,7 +120,7 @@ const PLATFORM_CAPABILITIES = Object.freeze({
117
120
  sharedActorRuleScopes: Object.freeze(['user']),
118
121
  }),
119
122
  signal: capabilityTemplate({
120
- supportsMentionGate: false,
123
+ supportsMentionGate: true,
121
124
  supportsDiscovery: false,
122
125
  directRuleScopes: Object.freeze(['phone_number', 'user', 'chat']),
123
126
  sharedSpaceRuleScopes: Object.freeze(['group', 'chat']),
@@ -230,12 +233,15 @@ function defaultSharedPolicyForPlatform(platform) {
230
233
  function createDefaultAccessPolicy(platform) {
231
234
  const capabilities = getPlatformAccessCapabilities(platform);
232
235
  return {
236
+ schemaVersion: ACCESS_POLICY_SCHEMA_VERSION,
233
237
  directPolicy: 'allowlist',
234
238
  sharedPolicy: capabilities.supportsSharedPolicy ? 'allowlist' : 'disabled',
235
- requireMentionInShared: false,
239
+ defaultAllowUntaggedInShared: true,
236
240
  directRules: [],
237
241
  sharedSpaceRules: [],
238
242
  sharedActorRules: [],
243
+ sharedMemberRules: [],
244
+ sharedParticipationRules: [],
239
245
  };
240
246
  }
241
247
 
@@ -253,7 +259,12 @@ function dedupeRules(rules) {
253
259
  const seen = new Set();
254
260
  const result = [];
255
261
  for (const rule of rules) {
256
- const key = `${rule.scope}:${rule.value}`;
262
+ const key = [
263
+ rule.scope,
264
+ rule.value,
265
+ rule.spaceScope || '',
266
+ rule.spaceValue || '',
267
+ ].join(':');
257
268
  if (seen.has(key)) continue;
258
269
  seen.add(key);
259
270
  result.push(rule);
@@ -261,6 +272,31 @@ function dedupeRules(rules) {
261
272
  return result;
262
273
  }
263
274
 
275
+ function normalizeSharedMemberRule(rule, actorScopes, spaceScopes) {
276
+ const actorRule = normalizeRule(rule, actorScopes);
277
+ if (!actorRule) return null;
278
+ const spaceScope = normalizeScope(rule.spaceScope);
279
+ if (!RULE_SCOPE_SET.has(spaceScope) || !spaceScopes.has(spaceScope)) return null;
280
+ const spaceValue = sanitizeValue(spaceScope, rule.spaceValue);
281
+ if (!spaceValue) return null;
282
+ const spaceLabel = String(rule.spaceLabel || '').trim();
283
+ return {
284
+ ...actorRule,
285
+ spaceScope,
286
+ spaceValue,
287
+ ...(spaceLabel ? { spaceLabel } : {}),
288
+ };
289
+ }
290
+
291
+ function normalizeSharedParticipationRule(rule, spaceScopes) {
292
+ const normalized = normalizeRule(rule, spaceScopes);
293
+ if (!normalized) return null;
294
+ return {
295
+ ...normalized,
296
+ allowUntagged: rule.allowUntagged === true,
297
+ };
298
+ }
299
+
264
300
  function normalizeAccessPolicy(platform, value) {
265
301
  const capabilities = getPlatformAccessCapabilities(platform);
266
302
  const defaults = createDefaultAccessPolicy(platform);
@@ -268,28 +304,68 @@ function normalizeAccessPolicy(platform, value) {
268
304
  const sharedSpaceScopes = new Set(capabilities.sharedSpaceRuleScopes);
269
305
  const sharedActorScopes = new Set(capabilities.sharedActorRuleScopes);
270
306
  const raw = value && typeof value === 'object' ? value : {};
307
+ let rawSharedSpaceRules = Array.isArray(raw.sharedSpaceRules) ? raw.sharedSpaceRules : [];
308
+ let rawSharedActorRules = Array.isArray(raw.sharedActorRules) ? raw.sharedActorRules : [];
309
+ let rawSharedMemberRules = Array.isArray(raw.sharedMemberRules) ? raw.sharedMemberRules : [];
310
+ const rawSharedParticipationRules = Array.isArray(raw.sharedParticipationRules)
311
+ ? raw.sharedParticipationRules
312
+ : [];
313
+ let rawSharedPolicy = raw.sharedPolicy;
314
+ const isLegacyParticipationPolicy = Number(raw.schemaVersion || 0) < 3;
315
+
316
+ if (
317
+ Number(raw.schemaVersion || 0) < 2
318
+ && rawSharedMemberRules.length === 0
319
+ && rawSharedActorRules.length > 0
320
+ ) {
321
+ if (normalizeMode(rawSharedPolicy, defaults.sharedPolicy) === 'open') {
322
+ rawSharedPolicy = 'allowlist';
323
+ rawSharedSpaceRules = [];
324
+ } else if (rawSharedSpaceRules.length > 0) {
325
+ rawSharedMemberRules = rawSharedActorRules.flatMap((actorRule) =>
326
+ rawSharedSpaceRules.map((spaceRule) => ({
327
+ ...actorRule,
328
+ spaceScope: spaceRule.scope,
329
+ spaceValue: spaceRule.value,
330
+ spaceLabel: spaceRule.label,
331
+ })));
332
+ rawSharedSpaceRules = [];
333
+ rawSharedActorRules = [];
334
+ }
335
+ }
336
+
271
337
  const normalized = {
338
+ schemaVersion: ACCESS_POLICY_SCHEMA_VERSION,
272
339
  directPolicy: normalizeMode(raw.directPolicy, defaults.directPolicy),
273
- sharedPolicy: normalizeMode(raw.sharedPolicy, defaults.sharedPolicy),
274
- requireMentionInShared: capabilities.supportsMentionGate
275
- ? raw.requireMentionInShared === true
276
- : false,
340
+ sharedPolicy: normalizeMode(rawSharedPolicy, defaults.sharedPolicy),
341
+ defaultAllowUntaggedInShared: capabilities.supportsUntaggedGroupToggle
342
+ ? (isLegacyParticipationPolicy
343
+ ? raw.requireMentionInShared !== true
344
+ : raw.defaultAllowUntaggedInShared !== false)
345
+ : true,
277
346
  directRules: dedupeRules((Array.isArray(raw.directRules) ? raw.directRules : [])
278
347
  .map((rule) => normalizeRule(rule, directScopes))
279
348
  .filter(Boolean)),
280
- sharedSpaceRules: dedupeRules((Array.isArray(raw.sharedSpaceRules) ? raw.sharedSpaceRules : [])
349
+ sharedSpaceRules: dedupeRules(rawSharedSpaceRules
281
350
  .map((rule) => normalizeRule(rule, sharedSpaceScopes))
282
351
  .filter(Boolean)),
283
- sharedActorRules: dedupeRules((Array.isArray(raw.sharedActorRules) ? raw.sharedActorRules : [])
352
+ sharedActorRules: dedupeRules(rawSharedActorRules
284
353
  .map((rule) => normalizeRule(rule, sharedActorScopes))
285
354
  .filter(Boolean)),
355
+ sharedMemberRules: dedupeRules(rawSharedMemberRules
356
+ .map((rule) => normalizeSharedMemberRule(rule, sharedActorScopes, sharedSpaceScopes))
357
+ .filter(Boolean)),
358
+ sharedParticipationRules: dedupeRules(rawSharedParticipationRules
359
+ .map((rule) => normalizeSharedParticipationRule(rule, sharedSpaceScopes))
360
+ .filter(Boolean)),
286
361
  };
287
-
288
362
  if (!capabilities.supportsSharedPolicy) {
289
363
  normalized.sharedPolicy = 'disabled';
290
- normalized.requireMentionInShared = false;
364
+ normalized.defaultAllowUntaggedInShared = true;
291
365
  normalized.sharedSpaceRules = [];
292
366
  normalized.sharedActorRules = [];
367
+ normalized.sharedMemberRules = [];
368
+ normalized.sharedParticipationRules = [];
293
369
  }
294
370
 
295
371
  return normalized;
@@ -318,6 +394,8 @@ function migrateLegacyWhitelist(platform, entries) {
318
394
  directRules: [],
319
395
  sharedSpaceRules: [],
320
396
  sharedActorRules: [],
397
+ sharedMemberRules: [],
398
+ sharedParticipationRules: [],
321
399
  };
322
400
  const list = Array.isArray(entries)
323
401
  ? entries.map((item) => String(item || '').trim()).filter(Boolean)
@@ -421,6 +499,8 @@ function migrateLegacyWhitelist(platform, entries) {
421
499
  directRules: state.directRules,
422
500
  sharedSpaceRules: state.sharedSpaceRules,
423
501
  sharedActorRules: state.sharedActorRules,
502
+ sharedMemberRules: state.sharedMemberRules,
503
+ sharedParticipationRules: state.sharedParticipationRules,
424
504
  });
425
505
  }
426
506
 
@@ -474,6 +554,29 @@ function contextMatchesRule(rule, context) {
474
554
  }
475
555
  }
476
556
 
557
+ function contextMatchesSharedMemberRule(rule, context) {
558
+ if (!contextMatchesRule(rule, context)) return false;
559
+ return contextMatchesRule({
560
+ scope: rule.spaceScope,
561
+ value: rule.spaceValue,
562
+ }, context);
563
+ }
564
+
565
+ function allowUntaggedForContext(policy, context) {
566
+ const scopePriority = {
567
+ group: 4,
568
+ channel: 4,
569
+ room: 4,
570
+ chat: 3,
571
+ server: 2,
572
+ };
573
+ const rule = policy.sharedParticipationRules
574
+ .filter((item) => contextMatchesRule(item, context))
575
+ .sort((left, right) =>
576
+ (scopePriority[right.scope] || 0) - (scopePriority[left.scope] || 0))[0];
577
+ return rule ? rule.allowUntagged : policy.defaultAllowUntaggedInShared;
578
+ }
579
+
477
580
  function evaluateAccessPolicy(policyInput, context, platform) {
478
581
  const capabilities = getPlatformAccessCapabilities(platform);
479
582
  const policy = normalizeAccessPolicy(platform, policyInput);
@@ -485,7 +588,8 @@ function evaluateAccessPolicy(policyInput, context, platform) {
485
588
  return { allowed: false, reason: 'direct_disabled', policy };
486
589
  }
487
590
  if (policy.directPolicy === 'allowlist') {
488
- const directMatch = policy.directRules.some((rule) => contextMatchesRule(rule, context));
591
+ const directMatch = policy.directRules.some((rule) => contextMatchesRule(rule, context))
592
+ || policy.sharedActorRules.some((rule) => contextMatchesRule(rule, context));
489
593
  if (!directMatch) {
490
594
  return { allowed: false, reason: 'direct_not_allowed', policy };
491
595
  }
@@ -499,23 +603,21 @@ function evaluateAccessPolicy(policyInput, context, platform) {
499
603
  }
500
604
  if (policy.sharedPolicy === 'allowlist') {
501
605
  const sharedSpaceMatch = policy.sharedSpaceRules.some((rule) => contextMatchesRule(rule, context));
502
- if (!sharedSpaceMatch) {
503
- return { allowed: false, reason: 'shared_space_not_allowed', policy };
504
- }
505
- }
506
- if (policy.sharedActorRules.length > 0) {
507
606
  const sharedActorMatch = policy.sharedActorRules.some((rule) => contextMatchesRule(rule, context));
508
- if (!sharedActorMatch) {
509
- return { allowed: false, reason: 'shared_actor_not_allowed', policy };
607
+ const sharedMemberMatch = policy.sharedMemberRules.some(
608
+ (rule) => contextMatchesSharedMemberRule(rule, context),
609
+ );
610
+ if (!sharedSpaceMatch && !sharedActorMatch && !sharedMemberMatch) {
611
+ return { allowed: false, reason: 'shared_not_allowed', policy };
510
612
  }
511
613
  }
614
+ const allowUntagged = allowUntaggedForContext(policy, context);
512
615
  return {
513
616
  allowed: true,
514
617
  reason: 'allowed',
515
618
  policy,
516
- participationHint: capabilities.supportsMentionGate && policy.requireMentionInShared
517
- ? 'mention_only'
518
- : 'automatic',
619
+ allowUntagged,
620
+ participationHint: allowUntagged ? 'automatic' : 'mention_only',
519
621
  };
520
622
  }
521
623
 
@@ -533,27 +635,85 @@ function labelForScope(scope) {
533
635
  }
534
636
  }
535
637
 
536
- function makeSuggestion({ scope, value, label, bucket }) {
638
+ function makeSuggestion({
639
+ scope,
640
+ value,
641
+ label,
642
+ bucket,
643
+ spaceScope = null,
644
+ spaceValue = null,
645
+ spaceLabel = null,
646
+ }) {
537
647
  if (!scope || !value || !bucket) return null;
648
+ const rule = {
649
+ scope,
650
+ value,
651
+ ...(spaceScope && spaceValue ? { spaceScope, spaceValue } : {}),
652
+ ...(spaceLabel ? { spaceLabel } : {}),
653
+ };
538
654
  return {
539
655
  label: label || `Allow ${labelForScope(scope)} ${value}`,
540
656
  prefixedId: scope === 'phone_number' ? value : `${scope}:${value}`,
541
657
  bucket,
542
- rule: { scope, value },
658
+ rule,
543
659
  };
544
660
  }
545
661
 
662
+ function sharedSpaceFromContext(context, options = {}, allowedScopes = []) {
663
+ const scopes = new Set(allowedScopes);
664
+ if (context.channelId && scopes.has('channel')) {
665
+ return {
666
+ scope: 'channel',
667
+ value: context.channelId,
668
+ label: options.channelLabel || context.channelId,
669
+ };
670
+ }
671
+ if (context.roomId && scopes.has('room')) {
672
+ return {
673
+ scope: 'room',
674
+ value: context.roomId,
675
+ label: options.roomLabel || context.roomId,
676
+ };
677
+ }
678
+ if (context.groupId && scopes.has('group')) {
679
+ return {
680
+ scope: 'group',
681
+ value: context.groupId,
682
+ label: options.groupLabel || context.groupId,
683
+ };
684
+ }
685
+ if (context.chatId && scopes.has('chat')) {
686
+ return {
687
+ scope: 'chat',
688
+ value: context.chatId,
689
+ label: context.chatId,
690
+ };
691
+ }
692
+ return null;
693
+ }
694
+
546
695
  function buildBlockedSenderSuggestions(platform, context, options = {}) {
547
696
  const suggestions = [];
548
697
  const senderLabel = String(options.senderName || context.senderId || '').trim();
549
698
  const chatId = String(context.chatId || '').trim();
699
+ const capabilities = getPlatformAccessCapabilities(platform);
700
+ const actorScope = context.phoneNumber ? 'phone_number' : 'user';
701
+ const actorValue = String(context.phoneNumber || context.senderId || '').trim();
702
+ const canUseSharedActor = capabilities.sharedActorRuleScopes.includes(actorScope);
550
703
 
551
704
  if (context.isDirect) {
552
- if (context.phoneNumber) {
705
+ if (actorValue && canUseSharedActor) {
706
+ suggestions.push(makeSuggestion({
707
+ scope: actorScope,
708
+ value: actorValue,
709
+ label: `Allow sender everywhere (${senderLabel || actorValue})`,
710
+ bucket: 'sharedActorRules',
711
+ }));
712
+ } else if (context.phoneNumber) {
553
713
  suggestions.push(makeSuggestion({
554
714
  scope: 'phone_number',
555
715
  value: context.phoneNumber,
556
- label: `Allow number (${senderLabel || context.phoneNumber})`,
716
+ label: `Allow sender (${senderLabel || context.phoneNumber})`,
557
717
  bucket: 'directRules',
558
718
  }));
559
719
  } else if (context.senderId) {
@@ -573,56 +733,53 @@ function buildBlockedSenderSuggestions(platform, context, options = {}) {
573
733
  }));
574
734
  }
575
735
  } else if (context.isShared) {
576
- if (context.senderId) {
736
+ const sharedSpace = sharedSpaceFromContext(
737
+ context,
738
+ options,
739
+ capabilities.sharedSpaceRuleScopes,
740
+ );
741
+ if (actorValue && canUseSharedActor && sharedSpace) {
577
742
  suggestions.push(makeSuggestion({
578
- scope: context.phoneNumber ? 'phone_number' : 'user',
579
- value: context.phoneNumber || context.senderId,
580
- label: `Allow sender (${senderLabel || context.senderId})`,
581
- bucket: 'sharedActorRules',
743
+ scope: actorScope,
744
+ value: actorValue,
745
+ label: `Allow sender in this ${labelForScope(sharedSpace.scope)} only (${senderLabel || actorValue})`,
746
+ bucket: 'sharedMemberRules',
747
+ spaceScope: sharedSpace.scope,
748
+ spaceValue: sharedSpace.value,
749
+ spaceLabel: sharedSpace.label,
582
750
  }));
583
- }
584
- if (context.serverId) {
585
751
  suggestions.push(makeSuggestion({
586
- scope: 'server',
587
- value: context.serverId,
588
- label: `Allow server (${options.serverLabel || context.serverId})`,
589
- bucket: 'sharedSpaceRules',
752
+ scope: actorScope,
753
+ value: actorValue,
754
+ label: `Allow sender everywhere (${senderLabel || actorValue})`,
755
+ bucket: 'sharedActorRules',
590
756
  }));
591
757
  }
592
- if (context.channelId) {
593
- suggestions.push(makeSuggestion({
594
- scope: 'channel',
595
- value: context.channelId,
596
- label: `Allow channel (${options.channelLabel || context.channelId})`,
597
- bucket: 'sharedSpaceRules',
598
- }));
599
- } else if (context.roomId) {
600
- suggestions.push(makeSuggestion({
601
- scope: 'room',
602
- value: context.roomId,
603
- label: `Allow room (${options.roomLabel || context.roomId})`,
604
- bucket: 'sharedSpaceRules',
605
- }));
606
- } else if (context.groupId) {
758
+ if (sharedSpace) {
607
759
  suggestions.push(makeSuggestion({
608
- scope: 'group',
609
- value: context.groupId,
610
- label: `Allow group (${options.groupLabel || context.groupId})`,
611
- bucket: 'sharedSpaceRules',
612
- }));
613
- } else if (chatId) {
614
- suggestions.push(makeSuggestion({
615
- scope: 'chat',
616
- value: chatId,
617
- label: `Allow chat (${chatId})`,
760
+ scope: sharedSpace.scope,
761
+ value: sharedSpace.value,
762
+ label: `Allow everyone in this ${labelForScope(sharedSpace.scope)} (${sharedSpace.label})`,
618
763
  bucket: 'sharedSpaceRules',
619
764
  }));
620
765
  }
621
766
  }
622
767
 
623
768
  return suggestions.filter(Boolean).filter((item, index, list) => {
624
- const key = `${item.bucket}:${item.rule.scope}:${item.rule.value}`;
625
- return list.findIndex((entry) => `${entry.bucket}:${entry.rule.scope}:${entry.rule.value}` === key) === index;
769
+ const key = [
770
+ item.bucket,
771
+ item.rule.scope,
772
+ item.rule.value,
773
+ item.rule.spaceScope || '',
774
+ item.rule.spaceValue || '',
775
+ ].join(':');
776
+ return list.findIndex((entry) => [
777
+ entry.bucket,
778
+ entry.rule.scope,
779
+ entry.rule.value,
780
+ entry.rule.spaceScope || '',
781
+ entry.rule.spaceValue || '',
782
+ ].join(':') === key) === index;
626
783
  });
627
784
  }
628
785
 
@@ -652,11 +809,22 @@ function summarizeAccessPolicy(platform, policyInput) {
652
809
  ];
653
810
  if (capabilities.supportsSharedPolicy) {
654
811
  parts.push(`shared spaces ${policy.sharedPolicy}`);
655
- if (capabilities.supportsMentionGate) {
656
- parts.push(policy.requireMentionInShared ? 'mentions required' : 'mentions optional');
812
+ if (capabilities.supportsUntaggedGroupToggle) {
813
+ const mentionOnlyCount = policy.sharedParticipationRules
814
+ .filter((rule) => !rule.allowUntagged).length;
815
+ if (!policy.defaultAllowUntaggedInShared) {
816
+ parts.push('untagged responses off by default');
817
+ } else {
818
+ parts.push(mentionOnlyCount > 0
819
+ ? `${mentionOnlyCount} tagged-only shared space${mentionOnlyCount === 1 ? '' : 's'}`
820
+ : 'social intelligence for untagged messages');
821
+ }
657
822
  }
658
823
  }
659
- const ruleCount = policy.directRules.length + policy.sharedSpaceRules.length + policy.sharedActorRules.length;
824
+ const ruleCount = policy.directRules.length
825
+ + policy.sharedSpaceRules.length
826
+ + policy.sharedActorRules.length
827
+ + policy.sharedMemberRules.length;
660
828
  if (ruleCount > 0) {
661
829
  parts.push(`${ruleCount} rule${ruleCount == 1 ? '' : 's'}`);
662
830
  }
@@ -665,10 +833,29 @@ function summarizeAccessPolicy(platform, policyInput) {
665
833
 
666
834
  function classifyRecentTarget(platform, row) {
667
835
  const chatId = String(row.platform_chat_id || '').trim();
668
- const sender = String(row.sender || row.sender_id || '').trim();
669
836
  const metadata = row.metadata && typeof row.metadata === 'object' ? row.metadata : {};
837
+ const sender = String(
838
+ row.sender
839
+ || row.sender_id
840
+ || metadata.sender
841
+ || metadata.senderId
842
+ || metadata.sender_id
843
+ || '',
844
+ ).trim();
670
845
  const senderName = String(metadata.senderName || metadata.sender_name || row.sender_name || '').trim();
671
- const groupName = String(metadata.groupName || metadata.group_name || metadata.guildName || metadata.guild_name || '').trim();
846
+ const groupName = String(
847
+ metadata.groupName
848
+ || metadata.group_name
849
+ || metadata.channelName
850
+ || metadata.channel_name
851
+ || metadata.roomName
852
+ || metadata.room_name
853
+ || metadata.serverName
854
+ || metadata.server_name
855
+ || metadata.guildName
856
+ || metadata.guild_name
857
+ || '',
858
+ ).trim();
672
859
  if (!chatId && !sender) return null;
673
860
 
674
861
  const isDirect = !String(metadata.isGroup || '').match(/^(true|1)$/i) && (!chatId || chatId === sender || chatId === `dm_${sender}`);
@@ -718,6 +905,10 @@ function contextFromMessage(msg) {
718
905
  const chatId = String(msg.chatId || '').trim();
719
906
  const senderId = String(msg.sender || '').trim();
720
907
  const normalizedChatId = chatId.startsWith('dm_') ? chatId.slice(3) : chatId;
908
+ const capabilities = getPlatformAccessCapabilities(msg.platform);
909
+ const phoneNumber = capabilities.directRuleScopes.includes('phone_number')
910
+ ? normalizePhone(msg.phoneNumber || senderId)
911
+ : '';
721
912
  return {
722
913
  platform: msg.platform,
723
914
  senderId,
@@ -729,7 +920,7 @@ function contextFromMessage(msg) {
729
920
  serverId: String(msg.guildId || msg.serverId || '').trim(),
730
921
  roomId: String(msg.roomId || '').trim(),
731
922
  roleIds: Array.isArray(msg.roleIds) ? msg.roleIds.map(String) : [],
732
- phoneNumber: normalizePhone(msg.phoneNumber || senderId),
923
+ phoneNumber,
733
924
  wasMentioned: msg.wasMentioned === true,
734
925
  normalizedChatId,
735
926
  };
@@ -532,7 +532,7 @@ async function isAllowedMessagingSender({ io, userId, msg }) {
532
532
  const policy = parseStoredAccessPolicy(msg.platform, policyRow?.value, legacyRow?.value);
533
533
  const decision = evaluateAccessPolicy(policy, contextFromMessage(msg), msg.platform);
534
534
  if (decision.allowed) {
535
- msg.accessPolicyRequireMention = decision.policy?.requireMentionInShared === true;
535
+ msg.accessPolicyAllowUntagged = decision.allowUntagged !== false;
536
536
  return true;
537
537
  }
538
538