Skip to content

Commit 676f719

Browse files
authored
fix(copilot): resolve resource context through shared readers (#7627)
1 parent dd8b159 commit 676f719

12 files changed

Lines changed: 571 additions & 312 deletions

File tree

apps/sim/lib/copilot/application/execute-knowledge-use-case.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
COPILOT_APPLICATION_DELEGATION_TTL_MS,
66
type CopilotExecutionContext,
77
createCopilotApplicationPrincipal,
8-
createTrustedCopilotPrincipal,
8+
createCopilotChatPrincipal,
99
createTrustedOrganizationCopilotPrincipal,
1010
requireTrustedCopilotExecutionContext,
1111
requireTrustedOrganizationCopilotContext,
@@ -61,15 +61,7 @@ export function resolveCopilotKnowledgePrincipal(
6161
export function createCopilotChatKnowledgePrincipal(
6262
context: CopilotChatKnowledgeDelegationContext
6363
): DelegatedPrincipal {
64-
return createTrustedCopilotPrincipal(
65-
{
66-
userId: context.userId,
67-
workspaceId: context.workspaceId,
68-
delegationId: `copilot-chat:${context.chatId ?? context.workspaceId}`,
69-
chatId: context.chatId,
70-
},
71-
knowledgeDelegation
72-
)
64+
return createCopilotChatPrincipal(context, knowledgeDelegationPolicy.audience)
7365
}
7466

7567
/** Enters a registered knowledge application use case with trusted Copilot identity. */

apps/sim/lib/copilot/auth/application-delegation.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,24 @@ interface CreateTrustedCopilotPrincipalOptions {
6464
resourceScope?: CopilotResourceScope
6565
}
6666

67+
export interface CopilotChatDelegationContext {
68+
userId: string
69+
workspaceId: string
70+
chatId?: string
71+
}
72+
73+
/** Creates request-scoped authority for resource reads during an authenticated chat turn. */
74+
export function createCopilotChatPrincipal(
75+
context: CopilotChatDelegationContext,
76+
audience: string,
77+
resourceScope?: CopilotResourceScope
78+
): DelegatedPrincipal {
79+
return createTrustedCopilotPrincipal(
80+
{ ...context, delegationId: `copilot-chat:${context.chatId ?? context.workspaceId}` },
81+
{ audience, ttlMs: COPILOT_APPLICATION_DELEGATION_TTL_MS, resourceScope }
82+
)
83+
}
84+
6785
function requireNonEmpty(value: string | undefined, field: string): asserts value is string {
6886
if (!value?.trim()) throw new Error(`Copilot execution context requires ${field}`)
6987
}

apps/sim/lib/copilot/auth/file-delegation.ts

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
COPILOT_APPLICATION_DELEGATION_TTL_MS,
55
type CopilotExecutionContext,
66
createCopilotApplicationPrincipal,
7+
createCopilotChatPrincipal,
78
createTrustedCopilotPrincipal,
89
requireTrustedCopilotExecutionContext,
910
} from '@/lib/copilot/auth/application-delegation'
@@ -44,15 +45,7 @@ export function resolveCopilotFilePrincipal(
4445
export function createCopilotChatFilePrincipal(
4546
context: CopilotChatFileDelegationContext
4647
): DelegatedPrincipal {
47-
return createTrustedCopilotPrincipal(
48-
{
49-
userId: context.userId,
50-
workspaceId: context.workspaceId,
51-
delegationId: `copilot-chat:${context.chatId ?? context.workspaceId}`,
52-
chatId: context.chatId,
53-
},
54-
fileDelegation
55-
)
48+
return createCopilotChatPrincipal(context, workspaceFileDelegationPolicy.audience)
5649
}
5750

5851
/** Creates the principal used while materializing the Copilot workspace index. */

apps/sim/lib/copilot/auth/table-delegation.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
11
import { messageForCopilotApplicationError } from '@/lib/copilot/application/error'
22
import {
3-
COPILOT_APPLICATION_DELEGATION_TTL_MS,
43
type CopilotExecutionContext,
5-
createTrustedCopilotPrincipal,
4+
createCopilotChatPrincipal,
65
} from '@/lib/copilot/auth/application-delegation'
76
import { tableDelegationPolicy } from '@/lib/table/application/authorization'
87

@@ -13,14 +12,7 @@ export function createCopilotChatTablePrincipal(
1312
context: { userId: string; workspaceId: string; chatId?: string },
1413
tableId: string
1514
) {
16-
return createTrustedCopilotPrincipal(
17-
{ ...context, delegationId: `copilot-chat:${context.chatId ?? context.workspaceId}` },
18-
{
19-
audience: tableDelegationPolicy.audience,
20-
ttlMs: COPILOT_APPLICATION_DELEGATION_TTL_MS,
21-
resourceScope: { tableId },
22-
}
23-
)
15+
return createCopilotChatPrincipal(context, tableDelegationPolicy.audience, { tableId })
2416
}
2517

2618
export function messageForCopilotTableError(

apps/sim/lib/copilot/chat/folder-context.ts

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
import { createLogger } from '@sim/logger'
2-
import {
3-
COPILOT_APPLICATION_DELEGATION_TTL_MS,
4-
createTrustedCopilotPrincipal,
5-
} from '@/lib/copilot/auth/application-delegation'
2+
import { createCopilotChatPrincipal } from '@/lib/copilot/auth/application-delegation'
63
import { createCopilotChatFilePrincipal } from '@/lib/copilot/auth/file-delegation'
74
import { buildVfsFolderPathMap, encodeVfsPathSegments } from '@/lib/copilot/vfs/path-utils'
85
import { knowledgeDelegationPolicy } from '@/lib/knowledge/application/authorization'
@@ -17,9 +14,17 @@ import { parseWorkspaceFileFolderDisplayPath } from '@/lib/workspace-files/folde
1714
const logger = createLogger('ChatFolderContext')
1815

1916
const FOLDER_DOMAINS = {
20-
workflow: { root: 'workflows', policy: workflowDelegationPolicy },
21-
table: { root: 'tables', policy: tableDelegationPolicy },
22-
knowledge_base: { root: 'knowledgebases', policy: knowledgeDelegationPolicy },
17+
workflow: {
18+
root: 'workflows',
19+
policy: workflowDelegationPolicy,
20+
list: listWorkflowFolders,
21+
},
22+
table: { root: 'tables', policy: tableDelegationPolicy, list: listTableFoldersUseCase },
23+
knowledge_base: {
24+
root: 'knowledgebases',
25+
policy: knowledgeDelegationPolicy,
26+
list: listKnowledgeFolders,
27+
},
2328
} as const
2429

2530
type FolderDomain = keyof typeof FOLDER_DOMAINS
@@ -35,24 +40,12 @@ export function createChatFolderResolver(userId: string, workspaceId: string, ch
3540
const filePaths = new Map<string, Promise<string | null>>()
3641

3742
async function loadPaths(domain: FolderDomain): Promise<Map<string, string>> {
38-
const principal = createTrustedCopilotPrincipal(
39-
{ userId, workspaceId, chatId, delegationId: `copilot-chat:${chatId ?? workspaceId}` },
40-
{
41-
audience: FOLDER_DOMAINS[domain].policy.audience,
42-
ttlMs: COPILOT_APPLICATION_DELEGATION_TTL_MS,
43-
}
43+
const principal = createCopilotChatPrincipal(
44+
{ userId, workspaceId, chatId },
45+
FOLDER_DOMAINS[domain].policy.audience
4446
)
4547
const input = { workspaceId, sortBy: 'name', sortOrder: 'asc' } as const
46-
const { folders } = await (() => {
47-
switch (domain) {
48-
case 'workflow':
49-
return listWorkflowFolders.execute({ principal, input })
50-
case 'table':
51-
return listTableFoldersUseCase.execute({ principal, input })
52-
case 'knowledge_base':
53-
return listKnowledgeFolders.execute({ principal, input })
54-
}
55-
})()
48+
const { folders } = await FOLDER_DOMAINS[domain].list.execute({ principal, input })
5649
return buildVfsFolderPathMap(
5750
folders.map((folder) => ({
5851
folderId: folder.id,
@@ -93,7 +86,7 @@ export function createChatFolderResolver(userId: string, workspaceId: string, ch
9386
}
9487
return pending
9588
}
96-
const domains: FolderDomain[] = ['workflow', 'table', 'knowledge_base']
89+
const domains = Object.keys(FOLDER_DOMAINS) as FolderDomain[]
9790
const results = await Promise.allSettled(
9891
domains.map(async (domain) => {
9992
const path = await folderPath(folderId, domain)

apps/sim/lib/copilot/chat/post.test.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -801,6 +801,32 @@ describe('handleUnifiedChatPost', () => {
801801
expect(processContextsServer).not.toHaveBeenCalled()
802802
})
803803

804+
it('preserves the selected integration identifier through request parsing', async () => {
805+
const context = { kind: 'integration', blockType: 'slack', label: 'Slack' }
806+
const response = await handleUnifiedChatPost(
807+
new NextRequest('http://localhost/api/copilot/chat', {
808+
method: 'POST',
809+
body: JSON.stringify({
810+
message: '@Slack help me use this integration',
811+
workspaceId: 'ws-1',
812+
createNewChat: true,
813+
contexts: [context],
814+
resourceAttachments: [{ type: 'integration', id: 'slack', title: 'Slack', active: true }],
815+
}),
816+
})
817+
)
818+
819+
expect(response.status).toBe(200)
820+
expect(processContextsServer.mock.calls[0]?.[0]).toEqual([context])
821+
expect(resolveActiveResourceContext).toHaveBeenCalledWith(
822+
'integration',
823+
'slack',
824+
'ws-1',
825+
'user-1',
826+
expect.any(String)
827+
)
828+
})
829+
804830
it('forwards slash-selected MCP server ids to the request-local tool builder', async () => {
805831
const response = await handleUnifiedChatPost(
806832
new NextRequest('http://localhost/api/copilot/chat', {

apps/sim/lib/copilot/chat/post.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,7 @@ const ResourceAttachmentSchema = z.object({
109109
'table',
110110
'file',
111111
'knowledgebase',
112+
'integration',
112113
'folder',
113114
'filefolder',
114115
'task',
@@ -142,6 +143,7 @@ const GENERIC_RESOURCE_TITLE: Record<z.infer<typeof ResourceAttachmentSchema>['t
142143
table: 'Table',
143144
file: 'File',
144145
knowledgebase: 'Knowledge Base',
146+
integration: 'Integration',
145147
folder: 'Folder',
146148
filefolder: 'File Folder',
147149
task: 'Task',
@@ -240,6 +242,7 @@ const ChatContextSchema = z
240242
knowledgeId: z.string().optional(),
241243
blockId: z.string().optional(),
242244
blockIds: z.array(z.string()).optional(),
245+
blockType: z.string().min(1).max(200).optional(),
243246
executionId: z.string().optional(),
244247
tableId: z.string().optional(),
245248
fileId: z.string().optional(),

0 commit comments

Comments
 (0)