Skip to content

Commit aa6d13d

Browse files
authored
fix(copilot): clarify knowledge connector authentication (#8205)
1 parent aba70b2 commit aa6d13d

3 files changed

Lines changed: 86 additions & 26 deletions

File tree

‎apps/sim/lib/copilot/vfs/serializers.test.ts‎

Lines changed: 29 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,6 @@
22
* @vitest-environment node
33
*/
44
import { describe, expect, it } from 'vitest'
5-
import {
6-
MAX_SANDBOX_CLI_TOOLS,
7-
SANDBOX_CLI_TOOLS,
8-
SANDBOX_SELECTABLE_CLI_TOOL_IDS,
9-
} from '@/lib/execution/remote-sandbox/cli-tools'
10-
import type { BlockConfig } from '@/blocks/types'
11-
import { hostedKeyEnabledWhen } from '@/tools/hosting'
12-
import type { ToolConfig } from '@/tools/types'
135
import {
146
buildOrganizationReadme,
157
serializeAccessControl,
@@ -20,6 +12,8 @@ import {
2012
serializeApiKeyIntegrations,
2113
serializeBlockSchema,
2214
serializeConnectedAccounts,
15+
serializeConnectorOverview,
16+
serializeConnectorSchema,
2317
serializeConnectors,
2418
serializeCredentials,
2519
serializeDeployments,
@@ -36,7 +30,16 @@ import {
3630
serializeTableMeta,
3731
serializeWorkflowMeta,
3832
serializeWorkspaceForks,
39-
} from './serializers'
33+
} from '@/lib/copilot/vfs/serializers'
34+
import {
35+
MAX_SANDBOX_CLI_TOOLS,
36+
SANDBOX_CLI_TOOLS,
37+
SANDBOX_SELECTABLE_CLI_TOOL_IDS,
38+
} from '@/lib/execution/remote-sandbox/cli-tools'
39+
import type { BlockConfig } from '@/blocks/types'
40+
import { gitlabConnectorMeta } from '@/connectors/gitlab/meta'
41+
import { hostedKeyEnabledWhen } from '@/tools/hosting'
42+
import type { ToolConfig } from '@/tools/types'
4043

4144
function hostedTool(id: string, conditional = false): ToolConfig {
4245
return {
@@ -619,6 +622,23 @@ describe('serializeCredentials — type distinguishes reconnect flow', () => {
619622
})
620623
})
621624

625+
describe('connector setup guidance', () => {
626+
it('describes GitLab PAT setup without requiring an OAuth credential or administrator fields', () => {
627+
const schema = JSON.parse(serializeConnectorSchema(gitlabConnectorMeta))
628+
expect(schema.auth.mode).toBe('apiKey')
629+
expect(schema.configFields.filter((field: { required?: boolean }) => field.required)).toEqual([
630+
expect.objectContaining({ id: 'project' }),
631+
])
632+
633+
const overview = serializeConnectorOverview([gitlabConnectorMeta])
634+
expect(overview).toContain(
635+
'For API-key connectors, pass apiKey as a `{{SECRET_NAME}}` reference'
636+
)
637+
expect(overview).toContain('For OAuth connectors, pass a credentialId')
638+
expect(overview).not.toContain('the user must have an OAuth credential')
639+
})
640+
})
641+
622642
describe('serializeConnectors — cloneable references, never key material', () => {
623643
const now = new Date('2026-08-14T00:00:00.000Z')
624644

‎apps/sim/lib/copilot/vfs/serializers.ts‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -452,8 +452,9 @@ export function serializeConnectorOverview(connectors: SerializableConnectorConf
452452
'|------|------|---------------|-----------------|',
453453
...rows,
454454
'',
455-
'To add a connector, the user must have an OAuth credential for that provider.',
456-
'Check `environment/credentials.json` for available credential IDs.',
455+
'For OAuth connectors, pass a credentialId from `environment/credentials.json`.',
456+
'For API-key connectors, pass apiKey as a `{{SECRET_NAME}}` reference or a raw key. Do not require an OAuth credential.',
457+
'For connectors supporting both, choose one authentication method from the connector schema.',
457458
].join('\n')
458459
}
459460

‎apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts‎

Lines changed: 54 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,8 @@ vi.mock('@/lib/embeddings', async () => ({
4242
}))
4343

4444
import { decryptApiKey } from '@/lib/api-key/crypto'
45+
import { resolveBillingAttribution } from '@/lib/billing/core/billing-attribution'
46+
import { knowledgeBaseServerTool } from '@/lib/copilot/tools/server/knowledge/knowledge-base'
4547
import { encryptSecret } from '@/lib/core/security/encryption'
4648
import {
4749
createKnowledgeAclFixtureIds,
@@ -119,22 +121,58 @@ afterAll(async () => {
119121
await db.$client.end()
120122
})
121123

122-
it.each([input.apiKey, '{{GITLAB_PAT}}'])(
123-
'creates, syncs, edits, and searches a workspace GitLab source using %s',
124-
async (apiKey) => {
125-
const { connector } = await createKnowledgeConnector.execute({
126-
principal,
127-
input: { ...input, apiKey },
128-
})
129-
expect(connector.accessMode).toBe('workspace')
130-
expect(JSON.stringify(connector)).not.toContain(input.apiKey)
124+
it.each([
125+
{ surface: 'application', apiKey: input.apiKey },
126+
{ surface: 'application', apiKey: '{{GITLAB_PAT}}' },
127+
{ surface: 'mothership', apiKey: input.apiKey },
128+
{ surface: 'mothership', apiKey: '{{GITLAB_PAT}}' },
129+
])(
130+
'creates, syncs, edits, and searches a workspace GitLab source through $surface using $apiKey',
131+
async ({ surface, apiKey }) => {
132+
let connectorId: string
133+
if (surface === 'mothership') {
134+
const result = await knowledgeBaseServerTool.execute(
135+
{
136+
operation: 'add_connector',
137+
args: {
138+
knowledgeBaseId: ids.knowledgeBaseId,
139+
connectorType: 'gitlab',
140+
apiKey,
141+
sourceConfig,
142+
},
143+
},
144+
{
145+
userId: ids.aliceId,
146+
workspaceId: ids.workspaceId,
147+
chatId: generateId(),
148+
executionId: generateId(),
149+
toolCallId: generateId(),
150+
copilotToolExecution: true,
151+
billingAttribution: await resolveBillingAttribution({
152+
actorUserId: ids.aliceId,
153+
workspaceId: ids.workspaceId,
154+
}),
155+
}
156+
)
157+
expect(result.success, result.message).toBe(true)
158+
expect(JSON.stringify(result)).not.toContain(input.apiKey)
159+
if (typeof result.data?.id !== 'string') throw new Error('Expected a connector ID')
160+
connectorId = result.data.id
161+
} else {
162+
const { connector } = await createKnowledgeConnector.execute({
163+
principal,
164+
input: { ...input, apiKey },
165+
})
166+
expect(JSON.stringify(connector)).not.toContain(input.apiKey)
167+
connectorId = connector.id
168+
}
131169
await expect
132170
.poll(
133171
async () => {
134172
const [row] = await db
135173
.select()
136174
.from(knowledgeConnector)
137-
.where(eq(knowledgeConnector.id, connector.id))
175+
.where(eq(knowledgeConnector.id, connectorId))
138176
return { status: row.status, error: row.lastSyncError, synced: Boolean(row.lastSyncAt) }
139177
},
140178
{ timeout: 15000 }
@@ -143,20 +181,21 @@ it.each([input.apiKey, '{{GITLAB_PAT}}'])(
143181
const [stored] = await db
144182
.select()
145183
.from(knowledgeConnector)
146-
.where(eq(knowledgeConnector.id, connector.id))
184+
.where(eq(knowledgeConnector.id, connectorId))
185+
expect(stored.accessMode).toBe('workspace')
147186
expect(stored.syncIntervalMinutes).toBe(1440)
148187
expect(stored.encryptedApiKey).not.toBe(input.apiKey)
149188
expect((await decryptApiKey(stored.encryptedApiKey!)).decrypted).toBe(input.apiKey)
150189
expect(
151190
await db
152191
.select()
153192
.from(knowledgeConnectorPermissionSnapshot)
154-
.where(eq(knowledgeConnectorPermissionSnapshot.connectorId, connector.id))
193+
.where(eq(knowledgeConnectorPermissionSnapshot.connectorId, connectorId))
155194
).toEqual([])
156195
const docs = await db
157196
.select()
158197
.from(document)
159-
.where(and(eq(document.connectorId, connector.id), isNull(document.deletedAt)))
198+
.where(and(eq(document.connectorId, connectorId), isNull(document.deletedAt)))
160199
expect(docs).toHaveLength(1)
161200
expect(docs[0].externalId).toBe('file:orion.md')
162201
expect(docs[0].acl).toEqual(['ws'])
@@ -198,7 +237,7 @@ it.each([input.apiKey, '{{GITLAB_PAT}}'])(
198237
await updateKnowledgeConnector.execute({
199238
principal,
200239
input: {
201-
connectorId: connector.id,
240+
connectorId,
202241
updates: { sourceConfig: { ...sourceConfig, ref: 'master' } },
203242
},
204243
})
@@ -208,7 +247,7 @@ it.each([input.apiKey, '{{GITLAB_PAT}}'])(
208247
const [row] = await db
209248
.select()
210249
.from(knowledgeConnector)
211-
.where(eq(knowledgeConnector.id, connector.id))
250+
.where(eq(knowledgeConnector.id, connectorId))
212251
return {
213252
status: row.status,
214253
error: row.lastSyncError,

0 commit comments

Comments
 (0)