diff --git a/server/src/__tests__/github-operation-credentials.test.ts b/server/src/__tests__/github-operation-credentials.test.ts index 02d1c864bcd..d9d1ef0c9d6 100644 --- a/server/src/__tests__/github-operation-credentials.test.ts +++ b/server/src/__tests__/github-operation-credentials.test.ts @@ -6,6 +6,7 @@ import { createRuntimeToolsToken } from "../runtime-tools-token.js"; import { errorHandler } from "../middleware/index.js"; import { randomUUID } from "node:crypto"; import { eq } from "drizzle-orm"; +import { createDatabaseResponseCapture, installDatabaseResponseCapture } from "@paperclipai/db/response-capture"; import { afterAll, beforeAll, describe, expect, it, vi } from "vitest"; import { agents, @@ -192,6 +193,42 @@ const support = await getEmbeddedPostgresTestSupport(); }); return { id, connectionId, secretId, definitionId }; } + it.each(["config", "transportConfig"] as const)("reads only current GitHub identity connections via %s", async location => { + const input = await seed(), selected = await grant(input, "A"); + const [connection] = await db.select().from(toolConnections).where(eq(toolConnections.id, selected.connectionId)); + await db.update(toolConnections).set({ config: {}, transportConfig: {}, [location]: { sourceTemplateKey: "github" } }) + .where(eq(toolConnections.id, selected.connectionId)); + const payload = "unrelated-connection-config🙂".repeat(10_000); + const irrelevant = [null, true, 42, ["github"], { name: "github" }, "GitHub", " github ", "other"].map(sourceTemplateKey => { + const id = randomUUID(); + return { id, companyId: input.companyId, applicationId: connection.applicationId, name: id, uid: id, + transport: "mcp_remote" as const, config: { sourceTemplateKey, payload }, transportConfig: { sourceTemplateKey } }; + }); + await db.insert(toolConnections).values(irrelevant); + const observed = createDb(database.connectionString), events: Array> = [], now = Date.now(); + const capture = createDatabaseResponseCapture({ start: now - 100, end: now + 59_000 }, event => events.push(event)); + installDatabaseResponseCapture(observed, { toolConnections }, capture); + try { + expect(await resolveManagedGitHubIdentitySelection(observed, input.companyId, { agentId: input.agentId, responsibleUserId: "A" })) + .toMatchObject({ configured: true, grant: { id: selected.id } }); + capture.flush(); + const reads = events.filter(event => event.table === "tool_connections" && event.operation === "select"); + expect(reads).toHaveLength(1); + expect(reads[0].rows).toBe(1); + expect(reads[0].responseValueBytes).toBeLessThan(8192); + await db.update(toolConnections).set({ enabled: false }).where(eq(toolConnections.id, selected.connectionId)); + expect((await resolveManagedGitHubIdentitySelection(observed, input.companyId, { agentId: input.agentId, responsibleUserId: "A" })).grant) + .toBeUndefined(); + await db.update(toolConnections).set({ [location]: { sourceTemplateKey: "other" } }).where(eq(toolConnections.id, selected.connectionId)); + expect(await resolveManagedGitHubIdentitySelection(observed, input.companyId, { agentId: input.agentId, responsibleUserId: "A" })) + .toEqual({ configured: false }); + expect(await resolveManagedGitHubIdentitySelection(observed, randomUUID(), { agentId: input.agentId, responsibleUserId: "A" })) + .toEqual({ configured: false }); + const saved = await db.select().from(toolConnections).where(eq(toolConnections.companyId, input.companyId)); + for (const row of irrelevant) expect(saved.find(candidate => candidate.id === row.id)?.config).toEqual(row.config); + } finally { capture.flush(); await observed.$client.end(); } + }); + async function switchTo( input: Awaited>, user: string, diff --git a/server/src/services/git-credentials.ts b/server/src/services/git-credentials.ts index 3fce7fc2b50..69bac2c029e 100644 --- a/server/src/services/git-credentials.ts +++ b/server/src/services/git-credentials.ts @@ -10,7 +10,7 @@ import { userSecretDefinitions, type Db, } from "@paperclipai/db"; -import { and, eq, inArray, or } from "drizzle-orm"; +import { and, eq, inArray, or, sql } from "drizzle-orm"; import { isGitHubDotCom } from "./github-fetch.js"; import { secretService } from "./secrets.js"; import { toolAccessService } from "./tool-access.js"; @@ -324,6 +324,10 @@ export async function resolveManagedGitHubIdentitySelection( }> { const connections = await db.select().from(toolConnections).where(and( eq(toolConnections.companyId, companyId), + or( + sql`${toolConnections.config}->'sourceTemplateKey' = '"github"'::jsonb`, + sql`${toolConnections.transportConfig}->'sourceTemplateKey' = '"github"'::jsonb`, + ), )); const githubConnections = connections.filter((connection) => { const config = connection.config && typeof connection.config === "object" ? connection.config as Record : {};