Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
668 changes: 665 additions & 3 deletions apps/agor-daemon/src/mcp/tools/gateway-channels.test.ts

Large diffs are not rendered by default.

460 changes: 402 additions & 58 deletions apps/agor-daemon/src/mcp/tools/gateway-channels.ts

Large diffs are not rendered by default.

20 changes: 20 additions & 0 deletions apps/agor-daemon/src/services/gateway.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -803,6 +803,26 @@ describe('GatewayService outbound emit session branch binding', () => {
expect(branchRepo.resolveUserPermission).toHaveBeenCalled();
expect(sendSlackMessage).not.toHaveBeenCalled();
});

it('plumbs threadTs through to the connector as thread_ts', async () => {
const { service, sendSlackMessage } = makeEmitHarness({});

await service.emitMessage(emitData({ threadTs: '171234.000100' }));

expect(sendSlackMessage).toHaveBeenCalledWith(
expect.objectContaining({ thread_ts: '171234.000100' })
);
});

it('omits thread_ts from the connector call when threadTs is not provided', async () => {
const { service, sendSlackMessage } = makeEmitHarness({});

await service.emitMessage(emitData());

expect(sendSlackMessage).toHaveBeenCalledWith(
expect.not.objectContaining({ thread_ts: expect.anything() })
);
});
});

describe('GatewayService Slack attachment ingestion', () => {
Expand Down
3 changes: 3 additions & 0 deletions apps/agor-daemon/src/services/gateway.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@ interface EmitGatewayMessageData {
gatewayChannelId: string;
message: string;
target?: string;
/** Optional Slack thread timestamp to reply into. Omit to start a new thread/DM message. */
threadTs?: string;
purpose?: string;
emittedByUserId: UserID;
/**
Expand Down Expand Up @@ -1477,6 +1479,7 @@ export class GatewayService {
channel: resolvedChannel,
text,
blocks,
...(data.threadTs ? { thread_ts: data.threadTs } : {}),
metadata: {
...(data.purpose ? { purpose: data.purpose } : {}),
...resolvedTargetMetadata,
Expand Down
66 changes: 66 additions & 0 deletions apps/agor-ui/src/components/SettingsModal/GatewayChannelsTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -401,6 +401,8 @@ const SLACK_PROBE_FIELDS = new Set<string>([
'ingest_files',
'agent_thread_history',
'agent_channel_history',
'agent_reactions',
'agent_file_upload',
'slack_public_scope',
'allowed_channel_ids',
]);
Expand Down Expand Up @@ -716,6 +718,10 @@ const SlackSetupWizard: React.FC<{
Form.useWatch('agent_thread_history', form) ?? SLACK_AGENT_TOOL_DEFAULTS.thread_history;
const agentChannelHistory =
Form.useWatch('agent_channel_history', form) ?? SLACK_AGENT_TOOL_DEFAULTS.channel_history;
const agentReactions =
Form.useWatch('agent_reactions', form) ?? SLACK_AGENT_TOOL_DEFAULTS.reactions;
const agentFileUpload =
Form.useWatch('agent_file_upload', form) ?? SLACK_AGENT_TOOL_DEFAULTS.file_upload;
const publicScope = (Form.useWatch('slack_public_scope', form) as string) ?? 'all';

const wizardOptions: SlackWizardOptions = useMemo(
Expand All @@ -730,6 +736,8 @@ const SlackSetupWizard: React.FC<{
agentTools: {
thread_history: agentThreadHistory,
channel_history: agentChannelHistory,
reactions: agentReactions,
file_upload: agentFileUpload,
},
}),
[
Expand All @@ -742,6 +750,8 @@ const SlackSetupWizard: React.FC<{
ingestFiles,
agentThreadHistory,
agentChannelHistory,
agentReactions,
agentFileUpload,
]
);

Expand Down Expand Up @@ -963,6 +973,26 @@ const SlackSetupWizard: React.FC<{
<Switch />
</Form.Item>

<Form.Item
label="Agents can add/remove reactions"
name="agent_reactions"
valuePropName="checked"
initialValue={false}
tooltip="Let session agents add/remove emoji reactions on Slack messages through the gateway MCP tools. Adds the reactions:write scope."
>
<Switch />
</Form.Item>

<Form.Item
label="Agents can upload files"
name="agent_file_upload"
valuePropName="checked"
initialValue={false}
tooltip="Let session agents upload files/images to a channel or thread through the gateway MCP tool. Adds the files:write scope."
>
<Switch />
</Form.Item>

<Form.Item
label="Enable outbound sends"
name="outbound_enabled"
Expand Down Expand Up @@ -1221,6 +1251,12 @@ const ChannelFormFields: React.FC<{
const agentChannelHistory = Boolean(
Form.useWatch('agent_channel_history', form) ?? storedAgentTools.channel_history
);
const agentReactions = Boolean(
Form.useWatch('agent_reactions', form) ?? storedAgentTools.reactions
);
const agentFileUpload = Boolean(
Form.useWatch('agent_file_upload', form) ?? storedAgentTools.file_upload
);
const alignGithubUsers = Form.useWatch('github_align_users', form) ?? false;
// Track the live Name field so the manifest preview reflects in-progress edits,
// falling back to the stored channel name.
Expand All @@ -1243,6 +1279,8 @@ const ChannelFormFields: React.FC<{
agentTools: {
thread_history: agentThreadHistory,
channel_history: agentChannelHistory,
reactions: agentReactions,
file_upload: agentFileUpload,
},
}),
[
Expand All @@ -1255,6 +1293,8 @@ const ChannelFormFields: React.FC<{
ingestFiles,
agentThreadHistory,
agentChannelHistory,
agentReactions,
agentFileUpload,
]
);
const slackScopes = useMemo(() => requiredBotScopes(slackOptions), [slackOptions]);
Expand Down Expand Up @@ -2184,6 +2224,26 @@ const ChannelFormFields: React.FC<{
<Switch />
</Form.Item>

<Form.Item
label="Agents can add/remove reactions"
name="agent_reactions"
valuePropName="checked"
initialValue={false}
tooltip="Let session agents add/remove emoji reactions on Slack messages through the gateway MCP tools. Requires the reactions:write scope."
>
<Switch />
</Form.Item>

<Form.Item
label="Agents can upload files"
name="agent_file_upload"
valuePropName="checked"
initialValue={false}
tooltip="Let session agents upload files/images to a channel or thread through the gateway MCP tool. Requires the files:write scope."
>
<Switch />
</Form.Item>

{sourcesEnabled && (
<CompactAlert
type="info"
Expand Down Expand Up @@ -2578,6 +2638,8 @@ export const GatewayChannelsTable: React.FC<GatewayChannelsTableProps> = ({
agent_tools: {
thread_history: values.agent_thread_history ?? SLACK_AGENT_TOOL_DEFAULTS.thread_history,
channel_history: values.agent_channel_history ?? SLACK_AGENT_TOOL_DEFAULTS.channel_history,
reactions: values.agent_reactions ?? SLACK_AGENT_TOOL_DEFAULTS.reactions,
file_upload: values.agent_file_upload ?? SLACK_AGENT_TOOL_DEFAULTS.file_upload,
},
};
setSlackTestLoading(true);
Expand Down Expand Up @@ -2737,6 +2799,8 @@ export const GatewayChannelsTable: React.FC<GatewayChannelsTableProps> = ({
config.agent_tools = {
thread_history: values.agent_thread_history ?? SLACK_AGENT_TOOL_DEFAULTS.thread_history,
channel_history: values.agent_channel_history ?? SLACK_AGENT_TOOL_DEFAULTS.channel_history,
reactions: values.agent_reactions ?? SLACK_AGENT_TOOL_DEFAULTS.reactions,
file_upload: values.agent_file_upload ?? SLACK_AGENT_TOOL_DEFAULTS.file_upload,
};
}

Expand Down Expand Up @@ -2900,6 +2964,8 @@ export const GatewayChannelsTable: React.FC<GatewayChannelsTableProps> = ({
const agentTools = resolveSlackAgentTools(config?.agent_tools);
formValues.agent_thread_history = agentTools.thread_history;
formValues.agent_channel_history = agentTools.channel_history;
formValues.agent_reactions = agentTools.reactions;
formValues.agent_file_upload = agentTools.file_upload;
} else if (channel.channel_type === 'github') {
formValues.github_app_id = config?.app_id;
formValues.github_installation_id = config?.installation_id;
Expand Down
69 changes: 65 additions & 4 deletions packages/core/src/gateway/connectors/slack-manifest.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,34 @@ describe('requiredBotScopes', () => {
);
});

it('adds reactions:write for the reactions capability and omits it otherwise', () => {
expect(requiredBotScopes(withOptions({ agentTools: { reactions: true } }))).toEqual([
'chat:write',
'im:history',
'im:read',
'reactions:write',
'users:read',
]);
expect(requiredBotScopes(baseOptions)).not.toContain('reactions:write');
expect(requiredBotScopes(withOptions({ agentTools: { reactions: false } }))).not.toContain(
'reactions:write'
);
});

it('adds files:write for the file_upload capability and omits it otherwise', () => {
expect(requiredBotScopes(withOptions({ agentTools: { file_upload: true } }))).toEqual([
'chat:write',
'files:write',
'im:history',
'im:read',
'users:read',
]);
expect(requiredBotScopes(baseOptions)).not.toContain('files:write');
expect(requiredBotScopes(withOptions({ agentTools: { file_upload: false } }))).not.toContain(
'files:write'
);
});

it('all capabilities on — de-duplicated and sorted', () => {
const allOn = withOptions({
publicChannels: true,
Expand All @@ -118,7 +146,12 @@ describe('requiredBotScopes', () => {
alignUsers: true,
outbound: true,
ingestFiles: true,
agentTools: { thread_history: true, channel_history: true },
agentTools: {
thread_history: true,
channel_history: true,
reactions: true,
file_upload: true,
},
});
expect(requiredBotScopes(allOn)).toEqual([
'app_mentions:read',
Expand All @@ -127,13 +160,15 @@ describe('requiredBotScopes', () => {
'chat:write',
'chat:write.public',
'files:read',
'files:write',
'groups:history',
'groups:read',
'im:history',
'im:read',
'im:write',
'mpim:history',
'mpim:read',
'reactions:write',
'users:read',
'users:read.email',
]);
Expand Down Expand Up @@ -162,36 +197,62 @@ describe('requiredBotScopes', () => {
});

describe('resolveSlackAgentTools', () => {
it('defaults thread_history ON and channel_history OFF for absent config', () => {
it('defaults thread_history ON and channel_history/reactions/file_upload OFF for absent config', () => {
expect(resolveSlackAgentTools(undefined)).toEqual({
thread_history: true,
channel_history: false,
reactions: false,
file_upload: false,
});
expect(resolveSlackAgentTools({})).toEqual({
thread_history: true,
channel_history: false,
reactions: false,
file_upload: false,
});
});

it('honors explicit values', () => {
expect(resolveSlackAgentTools({ thread_history: false, channel_history: true })).toEqual({
expect(
resolveSlackAgentTools({
thread_history: false,
channel_history: true,
reactions: true,
file_upload: true,
})
).toEqual({
thread_history: false,
channel_history: true,
reactions: true,
file_upload: true,
});
});

it('falls back to defaults for malformed config', () => {
expect(resolveSlackAgentTools('yes')).toEqual({
thread_history: true,
channel_history: false,
reactions: false,
file_upload: false,
});
expect(resolveSlackAgentTools({ thread_history: 'yes', channel_history: 1 })).toEqual({
expect(
resolveSlackAgentTools({
thread_history: 'yes',
channel_history: 1,
reactions: 'true',
file_upload: 0,
})
).toEqual({
thread_history: true,
channel_history: false,
reactions: false,
file_upload: false,
});
expect(resolveSlackAgentTools([true])).toEqual({
thread_history: true,
channel_history: false,
reactions: false,
file_upload: false,
});
});
});
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/gateway/connectors/slack-manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,8 @@ export interface SlackWizardOptions {
export const SLACK_AGENT_TOOL_SCOPES: Record<SlackAgentToolCapability, string[]> = {
thread_history: [],
channel_history: ['channels:history', 'groups:history', 'mpim:history'],
reactions: ['reactions:write'],
file_upload: ['files:write'],
};

export interface SlackBotEventSubscriptions {
Expand Down
Loading