@chrisromp/copilot-bridge 0.7.0 → 0.8.1
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.
- package/config.sample.json +9 -1
- package/dist/channels/slack/adapter.d.ts +62 -0
- package/dist/channels/slack/adapter.d.ts.map +1 -0
- package/dist/channels/slack/adapter.js +382 -0
- package/dist/channels/slack/adapter.js.map +1 -0
- package/dist/channels/slack/mrkdwn.d.ts +22 -0
- package/dist/channels/slack/mrkdwn.d.ts.map +1 -0
- package/dist/channels/slack/mrkdwn.js +120 -0
- package/dist/channels/slack/mrkdwn.js.map +1 -0
- package/dist/config.d.ts +5 -1
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +63 -7
- package/dist/config.js.map +1 -1
- package/dist/core/access-control.d.ts +32 -0
- package/dist/core/access-control.d.ts.map +1 -0
- package/dist/core/access-control.js +59 -0
- package/dist/core/access-control.js.map +1 -0
- package/dist/core/command-handler.d.ts +2 -0
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +75 -1
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/inter-agent.d.ts +9 -2
- package/dist/core/inter-agent.d.ts.map +1 -1
- package/dist/core/inter-agent.js +87 -22
- package/dist/core/inter-agent.js.map +1 -1
- package/dist/core/model-fallback.js +1 -1
- package/dist/core/model-fallback.js.map +1 -1
- package/dist/core/session-manager.d.ts +3 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +51 -13
- package/dist/core/session-manager.js.map +1 -1
- package/dist/index.js +207 -30
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +10 -1
- package/dist/types.d.ts.map +1 -1
- package/package.json +2 -1
- package/scripts/check.ts +54 -1
- package/scripts/com.copilot-bridge.plist +5 -2
- package/scripts/init.ts +322 -117
- package/scripts/install-service.ts +32 -1
- package/scripts/lib/config-gen.ts +74 -10
- package/scripts/lib/prerequisites.ts +17 -5
- package/scripts/lib/prompts.ts +4 -0
- package/scripts/lib/service.ts +27 -3
- package/scripts/lib/slack.ts +190 -0
- package/templates/admin/AGENTS.md +5 -5
- package/templates/agents/AGENTS.md +1 -1
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { loadConfig, getConfig, isConfiguredChannel, registerDynamicChannel, markChannelAsDM, getChannelConfig, getPlatformBots, getChannelBotName, isBotAdmin, getHardcodedRules, getConfigRules, reloadConfig, ConfigWatcher } from './config.js';
|
|
1
|
+
import { loadConfig, getConfig, isConfiguredChannel, registerDynamicChannel, markChannelAsDM, getChannelConfig, getPlatformBots, getPlatformAccess, getChannelBotName, isBotAdmin, getHardcodedRules, getConfigRules, reloadConfig, ConfigWatcher } from './config.js';
|
|
2
2
|
import { CopilotBridge } from './core/bridge.js';
|
|
3
3
|
import { SessionManager, BRIDGE_CUSTOM_TOOLS } from './core/session-manager.js';
|
|
4
4
|
import { handleCommand, parseCommand } from './core/command-handler.js';
|
|
@@ -12,7 +12,8 @@ import { initScheduler, stopAll as stopScheduler, listJobs, removeJob, pauseJob,
|
|
|
12
12
|
import { markBusy, markIdle, markIdleImmediate, isBusy, waitForChannelIdle, cancelIdleDebounce } from './core/channel-idle.js';
|
|
13
13
|
import { LoopDetector, MAX_IDENTICAL_CALLS } from './core/loop-detector.js';
|
|
14
14
|
import { getTaskHistory } from './state/store.js';
|
|
15
|
-
import {
|
|
15
|
+
import { checkUserAccess } from './core/access-control.js';
|
|
16
|
+
import { createLogger, setLogLevel } from './logger.js';
|
|
16
17
|
import fs from 'node:fs';
|
|
17
18
|
import path from 'node:path';
|
|
18
19
|
const log = createLogger('bridge');
|
|
@@ -116,16 +117,135 @@ function getAdapterForChannel(channelId) {
|
|
|
116
117
|
return null;
|
|
117
118
|
return { adapter, streaming };
|
|
118
119
|
}
|
|
120
|
+
const SLACK_UID_PATTERN = /^U[A-Z0-9]{6,}$/;
|
|
121
|
+
/**
|
|
122
|
+
* Resolve non-UID entries in Slack bot access configs.
|
|
123
|
+
* Handles added manually as usernames are looked up via Slack API (with pagination) and replaced with UIDs.
|
|
124
|
+
*/
|
|
125
|
+
async function resolveSlackAccessUsers(config) {
|
|
126
|
+
const slackPlatform = config.platforms.slack;
|
|
127
|
+
if (!slackPlatform?.bots)
|
|
128
|
+
return;
|
|
129
|
+
// Collect all access configs that need resolution: platform-level + per-bot
|
|
130
|
+
const accessTargets = [];
|
|
131
|
+
const firstBotToken = Object.values(slackPlatform.bots)[0]?.token;
|
|
132
|
+
if (slackPlatform.access?.users?.length && firstBotToken) {
|
|
133
|
+
accessTargets.push({ label: 'platform "slack"', access: slackPlatform.access, tokenSource: firstBotToken });
|
|
134
|
+
}
|
|
135
|
+
for (const [botName, bot] of Object.entries(slackPlatform.bots)) {
|
|
136
|
+
if (bot.access?.users?.length) {
|
|
137
|
+
accessTargets.push({ label: `bot "${botName}"`, access: bot.access, tokenSource: bot.token });
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
if (accessTargets.length === 0)
|
|
141
|
+
return;
|
|
142
|
+
// Deduplicate API calls — group by token
|
|
143
|
+
const membersByToken = new Map();
|
|
144
|
+
for (const target of accessTargets) {
|
|
145
|
+
if (membersByToken.has(target.tokenSource))
|
|
146
|
+
continue;
|
|
147
|
+
const unresolved = target.access.users.filter(u => !SLACK_UID_PATTERN.test(u));
|
|
148
|
+
if (unresolved.length === 0)
|
|
149
|
+
continue;
|
|
150
|
+
const allMembers = [];
|
|
151
|
+
try {
|
|
152
|
+
let cursor;
|
|
153
|
+
do {
|
|
154
|
+
const params = new URLSearchParams({ limit: '200' });
|
|
155
|
+
if (cursor)
|
|
156
|
+
params.set('cursor', cursor);
|
|
157
|
+
const resp = await fetch(`https://slack.com/api/users.list?${params}`, {
|
|
158
|
+
headers: { 'Authorization': `Bearer ${target.tokenSource}` },
|
|
159
|
+
});
|
|
160
|
+
if (!resp.ok) {
|
|
161
|
+
log.warn(` Slack users.list failed: HTTP ${resp.status}`);
|
|
162
|
+
break;
|
|
163
|
+
}
|
|
164
|
+
const data = await resp.json();
|
|
165
|
+
if (!data.ok) {
|
|
166
|
+
log.warn(` Slack users.list failed: ${data.error}`);
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
for (const m of data.members ?? [])
|
|
170
|
+
allMembers.push(m);
|
|
171
|
+
cursor = data.response_metadata?.next_cursor || undefined;
|
|
172
|
+
} while (cursor);
|
|
173
|
+
}
|
|
174
|
+
catch (err) {
|
|
175
|
+
log.warn(` Failed to fetch Slack users: ${err.message}`);
|
|
176
|
+
}
|
|
177
|
+
membersByToken.set(target.tokenSource, allMembers);
|
|
178
|
+
}
|
|
179
|
+
// Resolve each access config
|
|
180
|
+
for (const target of accessTargets) {
|
|
181
|
+
const unresolved = target.access.users.filter(u => !SLACK_UID_PATTERN.test(u));
|
|
182
|
+
if (unresolved.length === 0)
|
|
183
|
+
continue;
|
|
184
|
+
log.info(`Resolving ${unresolved.length} Slack handle(s) for ${target.label} access list...`);
|
|
185
|
+
const allMembers = membersByToken.get(target.tokenSource) ?? [];
|
|
186
|
+
// Build lookup map for O(1) resolution
|
|
187
|
+
const nameMap = new Map();
|
|
188
|
+
const displayMap = new Map();
|
|
189
|
+
for (const m of allMembers) {
|
|
190
|
+
if (m.deleted || m.is_bot)
|
|
191
|
+
continue;
|
|
192
|
+
const name = (m.name ?? '').toLowerCase();
|
|
193
|
+
if (name)
|
|
194
|
+
nameMap.set(name, m.id);
|
|
195
|
+
const displayName = m.profile?.display_name_normalized?.toLowerCase() ?? '';
|
|
196
|
+
if (displayName)
|
|
197
|
+
displayMap.set(displayName, m.id);
|
|
198
|
+
const realName = m.profile?.real_name_normalized?.toLowerCase() ?? '';
|
|
199
|
+
if (realName)
|
|
200
|
+
displayMap.set(realName, m.id);
|
|
201
|
+
}
|
|
202
|
+
const resolved = [];
|
|
203
|
+
for (const handle of unresolved) {
|
|
204
|
+
const normalized = handle.replace(/^@/, '').toLowerCase();
|
|
205
|
+
const byName = nameMap.get(normalized);
|
|
206
|
+
if (byName) {
|
|
207
|
+
log.info(` Resolved "${handle}" → ${byName} (by handle)`);
|
|
208
|
+
resolved.push(byName);
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const byDisplay = displayMap.get(normalized);
|
|
212
|
+
if (byDisplay) {
|
|
213
|
+
log.warn(` Resolved "${handle}" → ${byDisplay} (by display/real name — consider using the exact Slack handle for reliability)`);
|
|
214
|
+
resolved.push(byDisplay);
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
log.warn(` Could not resolve Slack handle "${handle}" — keeping as-is`);
|
|
218
|
+
resolved.push(handle);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const uidEntries = target.access.users.filter(u => SLACK_UID_PATTERN.test(u));
|
|
223
|
+
target.access.users = [...uidEntries, ...resolved];
|
|
224
|
+
}
|
|
225
|
+
}
|
|
119
226
|
async function main() {
|
|
120
227
|
log.info('copilot-bridge starting...');
|
|
121
228
|
// Load configuration
|
|
122
229
|
const config = loadConfig();
|
|
230
|
+
setLogLevel(config.logLevel ?? 'info');
|
|
123
231
|
log.info(`Loaded ${config.channels.length} channel mapping(s)`);
|
|
124
232
|
// Start config file watcher for hot-reload
|
|
125
233
|
const configWatcher = new ConfigWatcher();
|
|
126
234
|
configWatcher.onReload((result) => {
|
|
127
235
|
if (!result.success)
|
|
128
236
|
return;
|
|
237
|
+
// Re-apply logLevel in case config changed it
|
|
238
|
+
setLogLevel(getConfig().logLevel ?? 'info');
|
|
239
|
+
// Re-resolve Slack access handles after reload (config was re-read from disk).
|
|
240
|
+
// Fires asynchronously — messages during resolution use the old resolved values.
|
|
241
|
+
void (async () => {
|
|
242
|
+
try {
|
|
243
|
+
await resolveSlackAccessUsers(getConfig());
|
|
244
|
+
}
|
|
245
|
+
catch (err) {
|
|
246
|
+
log.warn(`Slack access resolution after reload failed: ${err.message}`);
|
|
247
|
+
}
|
|
248
|
+
})();
|
|
129
249
|
if (result.restartNeeded.length > 0) {
|
|
130
250
|
// Notify admin channels about restart-needed changes
|
|
131
251
|
for (const [key, adapter] of botAdapters) {
|
|
@@ -174,20 +294,44 @@ async function main() {
|
|
|
174
294
|
};
|
|
175
295
|
// Initialize channel adapters — one per bot identity
|
|
176
296
|
for (const [platformName, platformConfig] of Object.entries(config.platforms)) {
|
|
177
|
-
const factory = adapterFactories[platformName];
|
|
178
|
-
if (!factory) {
|
|
179
|
-
log.warn(`No adapter for platform "${platformName}" — skipping`);
|
|
180
|
-
continue;
|
|
181
|
-
}
|
|
182
297
|
const bots = getPlatformBots(platformName);
|
|
183
298
|
for (const [botName, botInfo] of bots) {
|
|
184
299
|
const key = `${platformName}:${botName}`;
|
|
185
|
-
|
|
300
|
+
let adapter;
|
|
301
|
+
if (platformName === 'slack') {
|
|
302
|
+
// Slack needs appToken for Socket Mode — construct directly
|
|
303
|
+
if (!botInfo.appToken) {
|
|
304
|
+
log.error(`Slack bot "${botName}" missing appToken — skipping`);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const { SlackAdapter } = await import('./channels/slack/adapter.js');
|
|
309
|
+
adapter = new SlackAdapter({
|
|
310
|
+
platformName,
|
|
311
|
+
botToken: botInfo.token,
|
|
312
|
+
appToken: botInfo.appToken,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
catch (err) {
|
|
316
|
+
log.error(`Failed to load Slack adapter: ${err.message}`);
|
|
317
|
+
continue;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
else {
|
|
321
|
+
const factory = adapterFactories[platformName];
|
|
322
|
+
if (!factory) {
|
|
323
|
+
log.warn(`No adapter for platform "${platformName}" — skipping`);
|
|
324
|
+
break; // skip all bots for this platform
|
|
325
|
+
}
|
|
326
|
+
adapter = factory(platformName, platformConfig.url ?? '', botInfo.token);
|
|
327
|
+
}
|
|
186
328
|
botAdapters.set(key, adapter);
|
|
187
329
|
botStreamers.set(key, new StreamingHandler(adapter));
|
|
188
330
|
log.info(`Registered bot "${botName}" for ${platformName}`);
|
|
189
331
|
}
|
|
190
332
|
}
|
|
333
|
+
// Resolve non-UID Slack access entries at startup
|
|
334
|
+
await resolveSlackAccessUsers(config);
|
|
191
335
|
// Wire up session events → streaming output (serialized per channel)
|
|
192
336
|
sessionManager.onSessionEvent((sessionId, channelId, event) => {
|
|
193
337
|
const prev = eventLocks.get(channelId) ?? Promise.resolve();
|
|
@@ -242,7 +386,7 @@ async function main() {
|
|
|
242
386
|
.catch(err => log.error(`Unhandled error in message handler:`, err)));
|
|
243
387
|
channelLocks.set(msg.channelId, next);
|
|
244
388
|
});
|
|
245
|
-
adapter.onReaction((reaction) => handleReaction(reaction, sessionManager));
|
|
389
|
+
adapter.onReaction((reaction) => handleReaction(reaction, sessionManager, platformName, botName));
|
|
246
390
|
await adapter.connect();
|
|
247
391
|
log.info(`${key} connected`);
|
|
248
392
|
// Discover existing DM channels and auto-register any that aren't configured
|
|
@@ -370,6 +514,12 @@ async function handleMidTurnMessage(msg, sessionManager, platformName, botName)
|
|
|
370
514
|
if (key.startsWith(`${platformName}:`) && msg.userId === a.getBotUserId())
|
|
371
515
|
return;
|
|
372
516
|
}
|
|
517
|
+
// Check user-level access control
|
|
518
|
+
const botInfo = getPlatformBots(platformName).get(botName);
|
|
519
|
+
if (!checkUserAccess(msg.userId, msg.username, botInfo?.access, getPlatformAccess(platformName))) {
|
|
520
|
+
log.debug(`User ${msg.username} (${msg.userId}) denied mid-turn access to bot "${botName}"`);
|
|
521
|
+
return;
|
|
522
|
+
}
|
|
373
523
|
if (!isConfiguredChannel(msg.channelId))
|
|
374
524
|
return;
|
|
375
525
|
const assignedBot = getChannelBotName(msg.channelId);
|
|
@@ -381,8 +531,10 @@ async function handleMidTurnMessage(msg, sessionManager, platformName, botName)
|
|
|
381
531
|
const { adapter } = resolved;
|
|
382
532
|
const channelConfig = getChannelConfig(msg.channelId);
|
|
383
533
|
// Respect trigger mode — don't steer on unmentioned messages in mention-only channels
|
|
384
|
-
if (channelConfig.triggerMode === 'mention' && !msg.mentionsBot && !msg.isDM)
|
|
534
|
+
if (channelConfig.triggerMode === 'mention' && !msg.mentionsBot && !msg.isDM) {
|
|
535
|
+
log.debug(`Ignoring mid-turn message (trigger=mention, no mention) in ${msg.channelId.slice(0, 8)}...`);
|
|
385
536
|
return;
|
|
537
|
+
}
|
|
386
538
|
const text = stripBotMention(msg.text, channelConfig.bot);
|
|
387
539
|
if (!text && !msg.attachments?.length)
|
|
388
540
|
return;
|
|
@@ -457,7 +609,7 @@ async function handleMidTurnMessage(msg, sessionManager, platformName, botName)
|
|
|
457
609
|
// Commands with complex action handlers (skills, schedule, rules) defer to serialized path.
|
|
458
610
|
const SAFE_MID_TURN = new Set([
|
|
459
611
|
'context', 'status', 'help', 'verbose', 'autopilot', 'yolo',
|
|
460
|
-
'mcp', 'model', 'models', 'reasoning',
|
|
612
|
+
'mcp', 'model', 'models', 'reasoning', 'agents',
|
|
461
613
|
'streamer-mode', 'on-air',
|
|
462
614
|
]);
|
|
463
615
|
if (SAFE_MID_TURN.has(parsed.command)) {
|
|
@@ -523,6 +675,12 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
523
675
|
if (key.startsWith(`${platformName}:`) && msg.userId === a.getBotUserId())
|
|
524
676
|
return;
|
|
525
677
|
}
|
|
678
|
+
// Check user-level access control (reads live config — hot-reloadable)
|
|
679
|
+
const botInfo = getPlatformBots(platformName).get(botName);
|
|
680
|
+
if (!checkUserAccess(msg.userId, msg.username, botInfo?.access, getPlatformAccess(platformName))) {
|
|
681
|
+
log.debug(`User ${msg.username} (${msg.userId}) denied access to bot "${botName}"`);
|
|
682
|
+
return; // silent drop
|
|
683
|
+
}
|
|
526
684
|
// Auto-register DM channels for known bots
|
|
527
685
|
if (!isConfiguredChannel(msg.channelId) && msg.isDM) {
|
|
528
686
|
const workspacePath = getWorkspacePath(botName);
|
|
@@ -558,8 +716,10 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
558
716
|
const channelConfig = getChannelConfig(msg.channelId);
|
|
559
717
|
// Check trigger mode
|
|
560
718
|
const triggerMode = channelConfig.triggerMode;
|
|
561
|
-
if (triggerMode === 'mention' && !msg.mentionsBot && !msg.isDM)
|
|
719
|
+
if (triggerMode === 'mention' && !msg.mentionsBot && !msg.isDM) {
|
|
720
|
+
log.debug(`Ignoring message (trigger=mention, no mention) in ${msg.channelId.slice(0, 8)}...`);
|
|
562
721
|
return;
|
|
722
|
+
}
|
|
563
723
|
// Strip bot mention from message text
|
|
564
724
|
let text = stripBotMention(msg.text, channelConfig.bot);
|
|
565
725
|
if (!text && !msg.attachments?.length)
|
|
@@ -635,22 +795,26 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
635
795
|
if (!result.success) {
|
|
636
796
|
response = `❌ Config reload failed: ${result.error}\nExisting config is unchanged.`;
|
|
637
797
|
}
|
|
638
|
-
else if (result.changes.length === 0 && result.restartNeeded.length === 0) {
|
|
639
|
-
response = '✅ Config reloaded — no changes detected.';
|
|
640
|
-
}
|
|
641
798
|
else {
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
parts.push(` ✓ ${c}`);
|
|
799
|
+
// Re-apply logLevel after manual reload
|
|
800
|
+
setLogLevel(getConfig().logLevel ?? 'info');
|
|
801
|
+
if (result.changes.length === 0 && result.restartNeeded.length === 0) {
|
|
802
|
+
response = '✅ Config reloaded — no changes detected.';
|
|
647
803
|
}
|
|
648
|
-
|
|
649
|
-
parts
|
|
650
|
-
|
|
651
|
-
parts.push(
|
|
804
|
+
else {
|
|
805
|
+
const parts = ['✅ Config reloaded.'];
|
|
806
|
+
if (result.changes.length > 0) {
|
|
807
|
+
parts.push('**Applied:**');
|
|
808
|
+
for (const c of result.changes)
|
|
809
|
+
parts.push(` ✓ ${c}`);
|
|
810
|
+
}
|
|
811
|
+
if (result.restartNeeded.length > 0) {
|
|
812
|
+
parts.push('**Restart needed:**');
|
|
813
|
+
for (const r of result.restartNeeded)
|
|
814
|
+
parts.push(` ⚠️ ${r}`);
|
|
815
|
+
}
|
|
816
|
+
response = parts.join('\n');
|
|
652
817
|
}
|
|
653
|
-
response = parts.join('\n');
|
|
654
818
|
}
|
|
655
819
|
await adapter.sendMessage(msg.channelId, response, { threadRootId: threadRoot });
|
|
656
820
|
break;
|
|
@@ -910,14 +1074,16 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
910
1074
|
lines.push('**Skills**');
|
|
911
1075
|
for (const s of skills) {
|
|
912
1076
|
const desc = s.description ? ` — ${s.description}` : '';
|
|
913
|
-
|
|
1077
|
+
const flag = s.pending ? ' ⏳ _reload to activate_' : '';
|
|
1078
|
+
lines.push(`• \`${s.name}\`${desc} _(${s.source})_${flag}`);
|
|
914
1079
|
}
|
|
915
1080
|
lines.push('');
|
|
916
1081
|
}
|
|
917
1082
|
if (mcpInfo.length > 0) {
|
|
918
1083
|
lines.push('**MCP Servers**');
|
|
919
1084
|
for (const s of mcpInfo) {
|
|
920
|
-
|
|
1085
|
+
const flag = s.pending ? ' ⏳ _reload to activate_' : '';
|
|
1086
|
+
lines.push(`• \`${s.name}\` _(${s.source})_${flag}`);
|
|
921
1087
|
}
|
|
922
1088
|
lines.push('');
|
|
923
1089
|
}
|
|
@@ -1020,11 +1186,20 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
1020
1186
|
}
|
|
1021
1187
|
}
|
|
1022
1188
|
// --- Reaction Handling ---
|
|
1023
|
-
async function handleReaction(reaction, sessionManager) {
|
|
1189
|
+
async function handleReaction(reaction, sessionManager, platformName, botName) {
|
|
1024
1190
|
if (!isConfiguredChannel(reaction.channelId))
|
|
1025
1191
|
return;
|
|
1026
1192
|
if (reaction.action !== 'added')
|
|
1027
1193
|
return;
|
|
1194
|
+
// Check user-level access control.
|
|
1195
|
+
// Reactions only carry userId (no username), so this matches against userId only.
|
|
1196
|
+
// For Slack (UIDs stored in config), this is exact. For Mattermost (usernames in config),
|
|
1197
|
+
// this is best-effort — admin bots should use both username and user ID in allowlists.
|
|
1198
|
+
const botInfo = getPlatformBots(platformName).get(botName);
|
|
1199
|
+
if (!checkUserAccess(reaction.userId, reaction.userId, botInfo?.access, getPlatformAccess(platformName))) {
|
|
1200
|
+
log.debug(`User ${reaction.userId} denied reaction access to bot "${botName}"`);
|
|
1201
|
+
return;
|
|
1202
|
+
}
|
|
1028
1203
|
const resolved = getAdapterForChannel(reaction.channelId);
|
|
1029
1204
|
if (!resolved)
|
|
1030
1205
|
return;
|
|
@@ -1079,6 +1254,7 @@ async function handleSessionEvent(sessionId, channelId, event, sessionManager) {
|
|
|
1079
1254
|
// Handle custom bridge events (permissions, user input)
|
|
1080
1255
|
if (event.type === 'bridge.permission_request') {
|
|
1081
1256
|
const streamKey = activeStreams.get(channelId);
|
|
1257
|
+
const threadRootId = streamKey ? streaming.getStreamThreadRootId(streamKey) : undefined;
|
|
1082
1258
|
if (streamKey) {
|
|
1083
1259
|
await streaming.finalizeStream(streamKey);
|
|
1084
1260
|
activeStreams.delete(channelId);
|
|
@@ -1086,11 +1262,12 @@ async function handleSessionEvent(sessionId, channelId, event, sessionManager) {
|
|
|
1086
1262
|
await finalizeActivityFeed(channelId, adapter);
|
|
1087
1263
|
const { toolName, serverName, input, commands } = event.data;
|
|
1088
1264
|
const formatted = formatPermissionRequest(toolName, input, commands, serverName);
|
|
1089
|
-
await adapter.sendMessage(channelId, formatted);
|
|
1265
|
+
await adapter.sendMessage(channelId, formatted, { threadRootId });
|
|
1090
1266
|
return;
|
|
1091
1267
|
}
|
|
1092
1268
|
if (event.type === 'bridge.user_input_request') {
|
|
1093
1269
|
const streamKey = activeStreams.get(channelId);
|
|
1270
|
+
const threadRootId = streamKey ? streaming.getStreamThreadRootId(streamKey) : undefined;
|
|
1094
1271
|
if (streamKey) {
|
|
1095
1272
|
await streaming.finalizeStream(streamKey);
|
|
1096
1273
|
activeStreams.delete(channelId);
|
|
@@ -1098,7 +1275,7 @@ async function handleSessionEvent(sessionId, channelId, event, sessionManager) {
|
|
|
1098
1275
|
await finalizeActivityFeed(channelId, adapter);
|
|
1099
1276
|
const { question, choices } = event.data;
|
|
1100
1277
|
const formatted = formatUserInputRequest(question, choices);
|
|
1101
|
-
await adapter.sendMessage(channelId, formatted);
|
|
1278
|
+
await adapter.sendMessage(channelId, formatted, { threadRootId });
|
|
1102
1279
|
return;
|
|
1103
1280
|
}
|
|
1104
1281
|
// Format and route SDK events
|