Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -310,7 +310,7 @@ function StepConfigure({
{allSelected && (
<p className='text-[var(--text-muted)] text-caption'>
Full access — the bot can read and send messages, react, upload files, and chat as an AI
assistant.
agent.
</p>
)}
</div>
Expand Down
22 changes: 22 additions & 0 deletions apps/sim/lib/copilot/entitlements.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createLogger } from '@sim/logger'
import { getErrorMessage } from '@sim/utils/errors'
import { LRUCache } from 'lru-cache'
import { getBlockVisibilityForCopilot } from '@/lib/copilot/block-visibility'
import { isCustomBlocksEligible } from '@/lib/workflows/custom-blocks/operations'
import { SlackV2Block } from '@/blocks/blocks/slack'
import { isHiddenUnder } from '@/blocks/visibility/context'

const logger = createLogger('CopilotEntitlements')

Expand All @@ -10,6 +13,24 @@ const logger = createLogger('CopilotEntitlements')
* its `core.Entitlement*` constants to gate agent surfaces.
*/
export const CUSTOM_BLOCKS_ENTITLEMENT = 'custom-blocks'
export const SLACK_V2_ENTITLEMENT = 'slack-v2'

/**
* The slack_v2 surface (the preview block's tips, the workspace prompt's Slack
* tip listing, and the slack_custom_bot credential tag) follows the viewer's
* block-visibility projection — the same predicate that hides the slack_v2
* schema in the copilot VFS — so tip and schema visibility cannot diverge.
* Argument-order flip is deliberate: evaluators are (workspaceId, userId?),
* getBlockVisibilityForCopilot is (userId, workspaceId). No userId → fail
* closed (visibility is per-viewer; an anonymous evaluation must not reveal a
* preview block). Once slack_v2 GAs (preview flag dropped), this evaluator
* returns true everywhere without changes.
*/
async function isSlackV2Visible(workspaceId: string, userId?: string): Promise<boolean> {
if (!userId) return false
const vis = await getBlockVisibilityForCopilot(userId, workspaceId)
return !isHiddenUnder(vis, SlackV2Block)
}

/**
* Workspace entitlements — plan/flag-gated org capabilities sent to the
Expand All @@ -33,6 +54,7 @@ const ENTITLEMENT_EVALUATORS: Record<
(workspaceId: string, userId?: string) => Promise<boolean>
> = {
[CUSTOM_BLOCKS_ENTITLEMENT]: isCustomBlocksEligible,
[SLACK_V2_ENTITLEMENT]: isSlackV2Visible,
}

const entitlementsCache = new LRUCache<string, Promise<string[]>>({
Expand Down
12 changes: 12 additions & 0 deletions apps/sim/lib/webhooks/providers/slack.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,13 @@ interface SlackTriggerEvent {
* submissions). Null for non-block_actions payloads.
*/
state: Record<string, unknown> | null
/**
* What the user is currently viewing, for `app_context_changed` (agent
* messaging experience): `{ entities: [{ type, value, team_id }] }`, ordered
* by relevance; an empty object when Slack has no entities. Null for every
* other event type.
*/
context: Record<string, unknown> | null
hasFiles: boolean
files: SlackDownloadedFile[]
}
Expand Down Expand Up @@ -151,6 +158,7 @@ function createSlackEvent(): SlackTriggerEvent {
view: null,
message: null,
state: null,
context: null,
hasFiles: false,
files: [],
}
Expand Down Expand Up @@ -605,6 +613,7 @@ export function resolveSlackEventKey(body: Record<string, unknown>): string | nu
case 'pin_removed':
case 'team_join':
case 'app_home_opened':
case 'app_context_changed':
case 'assistant_thread_started':
case 'assistant_thread_context_changed':
return type
Expand Down Expand Up @@ -977,6 +986,9 @@ export const slackHandler: WebhookProviderHandler = {
event.message_ts = messageTs
event.hasFiles = hasFiles
event.files = files
if (eventType === 'app_context_changed') {
event.context = (rawEvent?.context as Record<string, unknown> | undefined) ?? {}
}

return { input: { event } }
},
Expand Down
26 changes: 20 additions & 6 deletions apps/sim/triggers/slack/capabilities.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ describe('buildSlackManifest - interactivity', () => {
})

describe('buildSlackManifest - description', () => {
it('emits display_information.description and reuses it as the assistant description', () => {
it('emits display_information.description and reuses it as the agent description', () => {
const manifest = buildSlackManifest(new Set(['action_assistant']), {
...opts,
description: 'Answers support questions.',
Expand All @@ -41,15 +41,29 @@ describe('buildSlackManifest - description', () => {
description: 'Answers support questions.',
})
const features = manifest.features as Record<string, Record<string, unknown>>
expect(features.assistant_view.assistant_description).toBe('Answers support questions.')
expect(features.agent_view.agent_description).toBe('Answers support questions.')
})

it('omits the description key and falls back for the assistant when absent', () => {
it('omits the description key and falls back for the agent when absent', () => {
const manifest = buildSlackManifest(new Set(['action_assistant']), opts)
expect(manifest.display_information).toEqual({ name: 'Test Bot' })
const features = manifest.features as Record<string, Record<string, unknown>>
expect(features.assistant_view.assistant_description).toBe(
'Test Bot — an AI assistant powered by Sim.'
)
expect(features.agent_view.agent_description).toBe('Test Bot — an AI agent powered by Sim.')
})

it('caps the agent description at the 300-char manifest limit', () => {
const manifest = buildSlackManifest(new Set(['action_assistant']), {
...opts,
description: 'x'.repeat(400),
})
const features = manifest.features as Record<string, Record<string, unknown>>
expect((features.agent_view.agent_description as string).length).toBe(300)
})

it('subscribes the agent-experience events, not the deprecated assistant_thread_* set', () => {
const manifest = buildSlackManifest(new Set(['action_assistant']), opts)
const settings = settingsOf(manifest)
const events = (settings.event_subscriptions as Record<string, unknown>).bot_events as string[]
expect(events).toEqual(['app_context_changed', 'app_home_opened', 'message.im'])
})
})
39 changes: 25 additions & 14 deletions apps/sim/triggers/slack/capabilities.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ export interface SlackCapability {
scopes: readonly string[]
events: readonly string[]
/**
* Marks the AI Assistant capability. When enabled the manifest additionally
* declares the app as an Agents & AI app (`features.assistant_view`) and
* enables the App Home messages tab — required for assistant threads, the
* "thinking" status (`assistant.threads.setStatus`), and DM-style chat to work.
* Marks the AI agent capability. When enabled the manifest additionally
* declares the app as an Agents & AI app (`features.agent_view` — the current
* agent messaging experience; new Slack apps cannot use the deprecated
* `assistant_view`) and enables the App Home messages tab — required for the
* agent surface, the "thinking" status (`assistant.threads.setStatus`), and
* DM-style chat to work.
*/
assistant?: boolean
/**
Expand Down Expand Up @@ -166,13 +168,18 @@ export const SLACK_CAPABILITIES: readonly SlackCapability[] = [
},
{
id: 'action_assistant',
label: 'AI assistant',
label: 'AI agent',
description:
'Register the bot as an AI assistant: users open an assistant thread, the bot shows a "thinking" status, and can set the thread title and suggested prompts (assistant.threads.*).',
'Register the bot as an AI agent: users chat with it in the agent surface, the bot shows a "thinking" status, and can set the thread title and suggested prompts (assistant.threads.*).',
defaultChecked: true,
group: 'action',
scopes: ['assistant:write', 'im:history'],
events: ['assistant_thread_started', 'assistant_thread_context_changed', 'message.im'],
// The agent messaging experience's event set: app_home_opened (user opened
// the agent DM), app_context_changed (what the user is viewing; requires
// features.agent_view), message.im (incoming messages). The legacy
// assistant_thread_* events belong to the deprecated assistant_view
// experience and are not subscribable by new agent_view apps.
events: ['app_home_opened', 'app_context_changed', 'message.im'],

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assistant triggers lack manifest events

Medium Severity

Custom-bot manifests with the AI agent capability now subscribe to app_home_opened, app_context_changed, and message.im instead of assistant_thread_*, but the trigger event picker still offers “Assistant opened” and “Assistant context changed.” Workflows using those events never receive webhooks from newly created agent apps.

Additional Locations (1)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b6aab2c. Configure here.

assistant: true,
},
{
Expand Down Expand Up @@ -211,7 +218,7 @@ const WEBHOOK_URL_PLACEHOLDER = '<deploy workflow to generate webhook URL>'
export interface BuildManifestOptions {
appName: string
webhookUrl: string | null
/** Shown on the bot's Slack profile and as the assistant description. */
/** Shown on the bot's Slack profile and as the agent description. */
description?: string
}

Expand Down Expand Up @@ -241,12 +248,16 @@ export function buildSlackManifest(
bot_user: { display_name: displayName, always_online: true },
}
if (isAssistant) {
// Declares the app as an Agents & AI app; without this Slack won't surface
// the assistant thread UI or fire assistant_thread_* events. The messages
// tab must be enabled so users can chat the assistant.
features.assistant_view = {
assistant_description:
trimmedDescription || `${displayName} — an AI assistant powered by Sim.`,
// Declares the app as an Agents & AI app via the current agent messaging
// experience. New Slack apps can ONLY use agent_view (assistant_view is
// deprecated and rejected for new apps; switching an existing app to
// agent_view is irreversible). Without this Slack won't surface the agent
// UI or fire app_context_changed. The messages tab must be enabled so
// users can chat with the agent. agent_description caps at 300 chars.
features.agent_view = {
agent_description: (
trimmedDescription || `${displayName} — an AI agent powered by Sim.`
).slice(0, 300),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Profile description exceeds agent cap

Medium Severity

The PR caps the user-supplied description at 300 characters for features.agent_view.agent_description, but the same text is copied verbatim into display_information.description. Long wizard descriptions can exceed Slack’s limit there and fail manifest validation even though the agent field was truncated.

Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit b6aab2c. Configure here.

}
features.app_home = {
home_tab_enabled: false,
Expand Down
11 changes: 11 additions & 0 deletions apps/sim/triggers/slack/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,11 @@ export const SLACK_TRIGGER_OUTPUTS: Record<string, TriggerOutput> = {
description:
'Timestamp of the message the interaction originated from. Present for block_actions',
},
context: {
type: 'json',
description:
'What the user is currently viewing: an object with an entities array (each entry: type, value, team_id), e.g. context.entities[0].value. Present for app_context_changed; null otherwise',
},
Comment on lines +130 to +134

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Context Output Shape Is Misstated

The normalizer returns the full context object, shaped like { entities: [...] }, but this description says the output is an entities array. Workflow authors following it may index event.context directly and receive no entity data.

Suggested change
context: {
type: 'json',
description:
'What the user is currently viewing, as an entities array (each entry: type, value, team_id). Present for app_context_changed; null otherwise',
},
context: {
type: 'json',
description:
'What the user is currently viewing, as an object containing an entities array (each entry: type, value, team_id). Present for app_context_changed; null otherwise',
},

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed — description now says it's an object with an entities array and shows the context.entities[0].value access path.

view: {
type: 'json',
description:
Expand Down Expand Up @@ -245,6 +250,12 @@ export const SLACK_EVENT_CATALOG: readonly SlackEventCatalogEntry[] = [
{ id: 'pin_removed', label: 'Pin removed', simSubscribed: false, filters: ['channels'] },
{ id: 'team_join', label: 'Member joined workspace', simSubscribed: false, filters: [] },
{ id: 'app_home_opened', label: 'App home opened', simSubscribed: false, filters: [] },
{
id: 'app_context_changed',
label: 'App context changed',
simSubscribed: false,
filters: [],
},
{ id: 'assistant_thread_started', label: 'Assistant opened', simSubscribed: true, filters: [] },
{
id: 'assistant_thread_context_changed',
Expand Down
Loading