From c2c4d18898c6e8c43bc919d6dc64e0eb00c71133 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Wed, 23 Sep 2026 09:41:55 -0700 Subject: [PATCH] fix(copilot): clarify knowledge connector authentication --- apps/sim/lib/copilot/vfs/serializers.test.ts | 38 +++++++--- apps/sim/lib/copilot/vfs/serializers.ts | 5 +- .../gitlab-workspace.integration.ts | 69 +++++++++++++++---- 3 files changed, 86 insertions(+), 26 deletions(-) diff --git a/apps/sim/lib/copilot/vfs/serializers.test.ts b/apps/sim/lib/copilot/vfs/serializers.test.ts index 7aa4a9dc961..c879e2fb2d3 100644 --- a/apps/sim/lib/copilot/vfs/serializers.test.ts +++ b/apps/sim/lib/copilot/vfs/serializers.test.ts @@ -2,14 +2,6 @@ * @vitest-environment node */ import { describe, expect, it } from 'vitest' -import { - MAX_SANDBOX_CLI_TOOLS, - SANDBOX_CLI_TOOLS, - SANDBOX_SELECTABLE_CLI_TOOL_IDS, -} from '@/lib/execution/remote-sandbox/cli-tools' -import type { BlockConfig } from '@/blocks/types' -import { hostedKeyEnabledWhen } from '@/tools/hosting' -import type { ToolConfig } from '@/tools/types' import { buildOrganizationReadme, serializeAccessControl, @@ -20,6 +12,8 @@ import { serializeApiKeyIntegrations, serializeBlockSchema, serializeConnectedAccounts, + serializeConnectorOverview, + serializeConnectorSchema, serializeConnectors, serializeCredentials, serializeDeployments, @@ -36,7 +30,16 @@ import { serializeTableMeta, serializeWorkflowMeta, serializeWorkspaceForks, -} from './serializers' +} from '@/lib/copilot/vfs/serializers' +import { + MAX_SANDBOX_CLI_TOOLS, + SANDBOX_CLI_TOOLS, + SANDBOX_SELECTABLE_CLI_TOOL_IDS, +} from '@/lib/execution/remote-sandbox/cli-tools' +import type { BlockConfig } from '@/blocks/types' +import { gitlabConnectorMeta } from '@/connectors/gitlab/meta' +import { hostedKeyEnabledWhen } from '@/tools/hosting' +import type { ToolConfig } from '@/tools/types' function hostedTool(id: string, conditional = false): ToolConfig { return { @@ -619,6 +622,23 @@ describe('serializeCredentials — type distinguishes reconnect flow', () => { }) }) +describe('connector setup guidance', () => { + it('describes GitLab PAT setup without requiring an OAuth credential or administrator fields', () => { + const schema = JSON.parse(serializeConnectorSchema(gitlabConnectorMeta)) + expect(schema.auth.mode).toBe('apiKey') + expect(schema.configFields.filter((field: { required?: boolean }) => field.required)).toEqual([ + expect.objectContaining({ id: 'project' }), + ]) + + const overview = serializeConnectorOverview([gitlabConnectorMeta]) + expect(overview).toContain( + 'For API-key connectors, pass apiKey as a `{{SECRET_NAME}}` reference' + ) + expect(overview).toContain('For OAuth connectors, pass a credentialId') + expect(overview).not.toContain('the user must have an OAuth credential') + }) +}) + describe('serializeConnectors — cloneable references, never key material', () => { const now = new Date('2026-08-14T00:00:00.000Z') diff --git a/apps/sim/lib/copilot/vfs/serializers.ts b/apps/sim/lib/copilot/vfs/serializers.ts index 437a703c80c..ccd315baaae 100644 --- a/apps/sim/lib/copilot/vfs/serializers.ts +++ b/apps/sim/lib/copilot/vfs/serializers.ts @@ -452,8 +452,9 @@ export function serializeConnectorOverview(connectors: SerializableConnectorConf '|------|------|---------------|-----------------|', ...rows, '', - 'To add a connector, the user must have an OAuth credential for that provider.', - 'Check `environment/credentials.json` for available credential IDs.', + 'For OAuth connectors, pass a credentialId from `environment/credentials.json`.', + 'For API-key connectors, pass apiKey as a `{{SECRET_NAME}}` reference or a raw key. Do not require an OAuth credential.', + 'For connectors supporting both, choose one authentication method from the connector schema.', ].join('\n') } diff --git a/apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts b/apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts index 5d2824a4c9b..2d7a2860a80 100644 --- a/apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts +++ b/apps/sim/lib/knowledge/__integration__/gitlab-workspace.integration.ts @@ -42,6 +42,8 @@ vi.mock('@/lib/embeddings', async () => ({ })) import { decryptApiKey } from '@/lib/api-key/crypto' +import { resolveBillingAttribution } from '@/lib/billing/core/billing-attribution' +import { knowledgeBaseServerTool } from '@/lib/copilot/tools/server/knowledge/knowledge-base' import { encryptSecret } from '@/lib/core/security/encryption' import { createKnowledgeAclFixtureIds, @@ -119,22 +121,58 @@ afterAll(async () => { await db.$client.end() }) -it.each([input.apiKey, '{{GITLAB_PAT}}'])( - 'creates, syncs, edits, and searches a workspace GitLab source using %s', - async (apiKey) => { - const { connector } = await createKnowledgeConnector.execute({ - principal, - input: { ...input, apiKey }, - }) - expect(connector.accessMode).toBe('workspace') - expect(JSON.stringify(connector)).not.toContain(input.apiKey) +it.each([ + { surface: 'application', apiKey: input.apiKey }, + { surface: 'application', apiKey: '{{GITLAB_PAT}}' }, + { surface: 'mothership', apiKey: input.apiKey }, + { surface: 'mothership', apiKey: '{{GITLAB_PAT}}' }, +])( + 'creates, syncs, edits, and searches a workspace GitLab source through $surface using $apiKey', + async ({ surface, apiKey }) => { + let connectorId: string + if (surface === 'mothership') { + const result = await knowledgeBaseServerTool.execute( + { + operation: 'add_connector', + args: { + knowledgeBaseId: ids.knowledgeBaseId, + connectorType: 'gitlab', + apiKey, + sourceConfig, + }, + }, + { + userId: ids.aliceId, + workspaceId: ids.workspaceId, + chatId: generateId(), + executionId: generateId(), + toolCallId: generateId(), + copilotToolExecution: true, + billingAttribution: await resolveBillingAttribution({ + actorUserId: ids.aliceId, + workspaceId: ids.workspaceId, + }), + } + ) + expect(result.success, result.message).toBe(true) + expect(JSON.stringify(result)).not.toContain(input.apiKey) + if (typeof result.data?.id !== 'string') throw new Error('Expected a connector ID') + connectorId = result.data.id + } else { + const { connector } = await createKnowledgeConnector.execute({ + principal, + input: { ...input, apiKey }, + }) + expect(JSON.stringify(connector)).not.toContain(input.apiKey) + connectorId = connector.id + } await expect .poll( async () => { const [row] = await db .select() .from(knowledgeConnector) - .where(eq(knowledgeConnector.id, connector.id)) + .where(eq(knowledgeConnector.id, connectorId)) return { status: row.status, error: row.lastSyncError, synced: Boolean(row.lastSyncAt) } }, { timeout: 15000 } @@ -143,7 +181,8 @@ it.each([input.apiKey, '{{GITLAB_PAT}}'])( const [stored] = await db .select() .from(knowledgeConnector) - .where(eq(knowledgeConnector.id, connector.id)) + .where(eq(knowledgeConnector.id, connectorId)) + expect(stored.accessMode).toBe('workspace') expect(stored.syncIntervalMinutes).toBe(1440) expect(stored.encryptedApiKey).not.toBe(input.apiKey) expect((await decryptApiKey(stored.encryptedApiKey!)).decrypted).toBe(input.apiKey) @@ -151,12 +190,12 @@ it.each([input.apiKey, '{{GITLAB_PAT}}'])( await db .select() .from(knowledgeConnectorPermissionSnapshot) - .where(eq(knowledgeConnectorPermissionSnapshot.connectorId, connector.id)) + .where(eq(knowledgeConnectorPermissionSnapshot.connectorId, connectorId)) ).toEqual([]) const docs = await db .select() .from(document) - .where(and(eq(document.connectorId, connector.id), isNull(document.deletedAt))) + .where(and(eq(document.connectorId, connectorId), isNull(document.deletedAt))) expect(docs).toHaveLength(1) expect(docs[0].externalId).toBe('file:orion.md') expect(docs[0].acl).toEqual(['ws']) @@ -198,7 +237,7 @@ it.each([input.apiKey, '{{GITLAB_PAT}}'])( await updateKnowledgeConnector.execute({ principal, input: { - connectorId: connector.id, + connectorId, updates: { sourceConfig: { ...sourceConfig, ref: 'master' } }, }, }) @@ -208,7 +247,7 @@ it.each([input.apiKey, '{{GITLAB_PAT}}'])( const [row] = await db .select() .from(knowledgeConnector) - .where(eq(knowledgeConnector.id, connector.id)) + .where(eq(knowledgeConnector.id, connectorId)) return { status: row.status, error: row.lastSyncError,