import { randomUUID } from "node:crypto"; import { eq } from "drizzle-orm"; import { afterAll, afterEach, beforeAll, describe, expect, it } from "vitest"; import { agents, activityLog, companies, companyMemberships, connectionGrants, createDb, heartbeatRuns, toolAccessAuditEvents, toolApplications, toolConnectionInstalls, toolConnections, toolCatalogEntries, toolMcpGateways, toolMcpGatewayTokens, toolProfileBindings, toolProfileEntries, toolProfiles, } from "@paperclipai/db"; import { getEmbeddedPostgresTestSupport, startEmbeddedPostgresTestDatabase, } from "./helpers/embedded-postgres.js"; import { buildPaperclipRuntimeMcpServers, createManagedMcpRunConfig } from "../services/heartbeat.js"; import { toolAccessService } from "../services/tool-access.js"; const embeddedPostgresSupport = await getEmbeddedPostgresTestSupport(); const describeEmbeddedPostgres = embeddedPostgresSupport.supported ? describe : describe.skip; describeEmbeddedPostgres("heartbeat runtime MCP servers", () => { let db!: ReturnType; let tempDb: Awaited> | null = null; const originalApiUrl = process.env.PAPERCLIP_API_URL; beforeAll(async () => { tempDb = await startEmbeddedPostgresTestDatabase("paperclip-heartbeat-runtime-mcp-"); db = createDb(tempDb.connectionString); }, 20_000); afterEach(async () => { if (originalApiUrl === undefined) delete process.env.PAPERCLIP_API_URL; else process.env.PAPERCLIP_API_URL = originalApiUrl; await db.delete(toolMcpGatewayTokens); await db.delete(activityLog); await db.delete(toolAccessAuditEvents); await db.delete(heartbeatRuns); await db.delete(toolMcpGateways); await db.delete(connectionGrants); await db.delete(toolConnectionInstalls); await db.delete(toolProfileBindings); await db.delete(toolProfileEntries); await db.delete(toolProfiles); await db.delete(toolConnections); await db.delete(toolApplications); await db.delete(agents); await db.delete(companyMemberships); await db.delete(companies); }); afterAll(async () => { await tempDb?.cleanup(); }); it("provisions one aggregate gateway and omits unavailable access without blocking any runtime", async () => { process.env.PAPERCLIP_API_URL = "https://paperclip.example.test"; const [company] = await db.insert(companies).values({ name: `Runtime MCP ${randomUUID()}`, issuePrefix: `RM${randomUUID().slice(0, 5).toUpperCase()}`, }).returning(); const [agent] = await db.insert(agents).values({ companyId: company!.id, name: "Runtime MCP Agent", role: "engineer", adapterType: "codex_local", adapterConfig: {}, }).returning(); const [application] = await db.insert(toolApplications).values({ companyId: company!.id, applicationKey: `runtime-${randomUUID().slice(0, 8)}`, name: "Runtime MCP App", type: "mcp_http", status: "active", }).returning(); const [installedConnection, uninstalledConnection] = await db.insert(toolConnections).values([ { companyId: company!.id, applicationId: application!.id, name: "Installed MCP", uid: `test/${randomUUID()}`, transport: "mcp_remote", status: "active", enabled: true, config: { url: "https://installed.example.test/mcp" }, }, { companyId: company!.id, applicationId: application!.id, name: "Uninstalled MCP", uid: `test/${randomUUID()}`, transport: "mcp_remote", status: "active", enabled: true, config: { url: "https://uninstalled.example.test/mcp" }, }, ]).returning(); const [profile] = await db.insert(toolProfiles).values({ companyId: company!.id, profileKey: `app:${installedConnection!.id}`, name: "Installed MCP", defaultAction: "deny", }).returning(); await db.insert(toolProfileEntries).values({ companyId: company!.id, profileId: profile!.id, selectorType: "connection", effect: "include", applicationId: application!.id, connectionId: installedConnection!.id, }); await db.insert(toolProfileBindings).values({ companyId: company!.id, profileId: profile!.id, targetType: "agent", targetId: agent!.id, }); await db.insert(toolConnectionInstalls).values({ companyId: company!.id, connectionId: installedConnection!.id, targetType: "agent", targetId: agent!.id, }); const before = Date.now(); const first = await buildPaperclipRuntimeMcpServers({ db, agent: agent!, runId: randomUUID() }); const second = await buildPaperclipRuntimeMcpServers({ db, agent: agent!, runId: randomUUID() }); expect(first).toHaveLength(1); expect(first[0]).toMatchObject({ name: "paperclip-assigned", connectionId: expect.stringMatching(/^assignment:[a-f0-9]{64}$/), url: expect.stringMatching(/^https:\/\/paperclip\.example\.test\/mcp\/gateways\/gw_[a-f0-9]{32}$/), token: expect.stringMatching(/^pcgw_/), }); expect(JSON.stringify(first)).not.toContain(uninstalledConnection!.id); expect(second).toHaveLength(1); expect(second[0]!.connectionId).toBe(first[0]!.connectionId); const gateways = await db.select().from(toolMcpGateways); expect(gateways).toHaveLength(1); expect(gateways[0]!.metadata).toMatchObject({ nativeRuntimeAssignmentDigest: first[0]!.connectionId.slice("assignment:".length), agentId: agent!.id, }); const tokens = await db.select().from(toolMcpGatewayTokens); expect(tokens).toHaveLength(2); for (const token of tokens) { expect(token.subjectType).toBe("heartbeat_run"); expect(token.subjectId).toMatch(/^[0-9a-f-]{36}$/); expect(token.expiresAt!.getTime()).toBeGreaterThanOrEqual(before + 59 * 60 * 1000); expect(token.expiresAt!.getTime()).toBeLessThanOrEqual(Date.now() + 61 * 60 * 1000); } expect(JSON.stringify(tokens)).not.toContain(first[0]!.token); await expect( buildPaperclipRuntimeMcpServers({ db, agent: agent!, runId: randomUUID(), expectedAssignmentDigest: "0".repeat(64), }), ).resolves.toEqual([]); expect(await db.select().from(toolMcpGatewayTokens)).toHaveLength(2); await db.update(toolConnections) .set({ healthStatus: "degraded", healthMessage: "fixture unavailable" }) .where(eq(toolConnections.id, installedConnection!.id)); const unavailableReports: Array> = []; await expect( buildPaperclipRuntimeMcpServers({ db, agent: agent!, runId: randomUUID(), expectedAssignmentDigest: first[0]!.connectionId.slice("assignment:".length), onUnavailableAssignedConnections: (connections) => { unavailableReports.push(connections); }, }), ).resolves.toEqual([]); expect(unavailableReports).toEqual([[ { id: installedConnection!.id, name: installedConnection!.name }, ]]); expect(await db.select().from(toolMcpGatewayTokens)).toHaveLength(2); await expect( createManagedMcpRunConfig({ db, agent: agent!, runId: randomUUID(), config: {}, projectId: null, issueId: null, }), ).resolves.toBeNull(); }); it("preserves exact permissions when an aggregate assignment exceeds the public 250-entry edit limit", async () => { process.env.PAPERCLIP_API_URL = "https://paperclip.example.test"; const [company] = await db.insert(companies).values({ name: "Large MCP assignment", issuePrefix: `LM${randomUUID().slice(0, 5).toUpperCase()}`, }).returning(); const [agent, gatewayReader] = await db.insert(agents).values([ { companyId: company!.id, name: "Cursor Cloud", role: "engineer", adapterType: "cursor_cloud" }, { companyId: company!.id, name: "Gateway reader", role: "engineer", adapterType: "cursor_cloud" }, ]).returning(); const [application] = await db.insert(toolApplications).values({ companyId: company!.id, applicationKey: "large-mcp", name: "Large MCP", type: "mcp_http", }).returning(); const [connection] = await db.insert(toolConnections).values({ companyId: company!.id, applicationId: application!.id, name: "Large MCP", uid: `test/${randomUUID()}`, transport: "mcp_remote", status: "active", enabled: true, config: { url: "https://large.example.test/mcp" }, }).returning(); const catalogInput = (name: string) => ({ companyId: company!.id, applicationId: application!.id, connectionId: connection!.id, name, toolName: name, versionHash: "fixture", status: "active" as const, }); const catalog = await db.insert(toolCatalogEntries).values([ ...Array.from({ length: 251 }, (_, index) => catalogInput(`allowed_${index}`)), catalogInput("excluded"), catalogInput("unassigned"), ]).returning(); const allowed = catalog.slice(0, 251); const excluded = catalog[251]!; // Multiple valid profiles can each have fewer than 250 entries while their // union exceeds the HTTP edit-request limit. const profiles = await db.insert(toolProfiles).values(["first", "second"].map((key) => ({ companyId: company!.id, profileKey: key, name: key, defaultAction: "deny" as const, }))).returning(); await db.insert(toolProfileEntries).values([ ...[...allowed, excluded].map((tool, index) => ({ companyId: company!.id, profileId: profiles[index < 200 ? 0 : 1]!.id, selectorType: "catalog_entry" as const, effect: "include" as const, applicationId: application!.id, connectionId: connection!.id, catalogEntryId: tool.id, })), { companyId: company!.id, profileId: profiles[1]!.id, selectorType: "catalog_entry" as const, effect: "exclude" as const, applicationId: application!.id, connectionId: connection!.id, catalogEntryId: excluded.id, }, ]); await db.insert(toolProfileBindings).values(profiles.map((profile) => ({ companyId: company!.id, profileId: profile.id, targetType: "agent" as const, targetId: agent!.id, }))); await db.insert(toolConnectionInstalls).values({ companyId: company!.id, connectionId: connection!.id, targetType: "agent", targetId: agent!.id, }); const servers = await buildPaperclipRuntimeMcpServers({ db, agent: agent!, runId: randomUUID() }); expect(servers).toHaveLength(1); const [gateway] = await db.select().from(toolMcpGateways); const generatedEntries = await db.select().from(toolProfileEntries) .where(eq(toolProfileEntries.profileId, gateway!.profileId!)); expect(generatedEntries).toHaveLength(251); expect(generatedEntries.every((entry) => entry.selectorType === "catalog_entry")).toBe(true); expect(generatedEntries.map((entry) => entry.catalogEntryId).sort()).toEqual(allowed.map((tool) => tool.id).sort()); // Evaluate the generated profile independently of the original assignments, // including a tool discovered after the immutable profile was created. await db.insert(toolCatalogEntries).values(catalogInput("new_after_snapshot")); await db.insert(toolProfileBindings).values({ companyId: company!.id, profileId: gateway!.profileId!, targetType: "agent", targetId: gatewayReader!.id, }); const effective = await toolAccessService(db).getEffectiveProfilesForAgent(company!.id, gatewayReader!.id); expect(effective.allowedTools.map((tool) => tool.id).sort()).toEqual(allowed.map((tool) => tool.id).sort()); expect(effective.allowedToolNames).not.toContain("excluded"); expect(effective.allowedToolNames).not.toContain("unassigned"); expect(effective.allowedToolNames).not.toContain("new_after_snapshot"); const reused = await buildPaperclipRuntimeMcpServers({ db, agent: agent!, runId: randomUUID() }); expect(reused[0]!.connectionId).toBe(servers[0]!.connectionId); expect(await db.select().from(toolMcpGateways)).toHaveLength(1); }); it("exposes only the dedicated GitHub connection when a personal connection is also installed", async () => { process.env.PAPERCLIP_API_URL = "https://paperclip.example.test"; const [company] = await db.insert(companies).values({ name: `Runtime GitHub identity ${randomUUID()}`, issuePrefix: `RG${randomUUID().slice(0, 5).toUpperCase()}`, }).returning(); await db.insert(companyMemberships).values({ companyId: company!.id, principalType: "user", principalId: "responsible-user", status: "active", membershipRole: "member", }); const [agent] = await db.insert(agents).values({ companyId: company!.id, name: "Dedicated GitHub Agent", role: "engineer", adapterType: "codex_local", adapterConfig: {}, }).returning(); const [application] = await db.insert(toolApplications).values({ companyId: company!.id, applicationKey: `github-${randomUUID().slice(0, 8)}`, name: "GitHub", type: "mcp_http", status: "active", metadata: { sourceTemplateKey: "github" }, }).returning(); const [personal, dedicated] = await db.insert(toolConnections).values([ { companyId: company!.id, applicationId: application!.id, name: "Responsible user's GitHub", uid: `github/${randomUUID()}`, transport: "mcp_remote", credentialPolicy: "per_user", status: "active", enabled: true, healthStatus: "ok", config: {}, transportConfig: { sourceTemplateKey: "github" }, }, { companyId: company!.id, applicationId: application!.id, name: "Dedicated GitHub", uid: `github/${randomUUID()}`, transport: "mcp_remote", credentialPolicy: "per_agent", status: "active", enabled: true, healthStatus: "ok", config: {}, transportConfig: { sourceTemplateKey: "github" }, }, ]).returning(); await db.insert(connectionGrants).values([ { companyId: company!.id, connectionId: personal!.id, kind: "user", subjectUserId: "responsible-user", status: "active", isDefault: false, }, { companyId: company!.id, connectionId: dedicated!.id, kind: "agent", subjectAgentId: agent!.id, status: "active", isDefault: false, }, ]); await db.insert(toolConnectionInstalls).values([ { companyId: company!.id, connectionId: personal!.id, targetType: "company", targetId: company!.id, }, { companyId: company!.id, connectionId: dedicated!.id, targetType: "agent", targetId: agent!.id, }, ]); const [profile] = await db.insert(toolProfiles).values({ companyId: company!.id, profileKey: `github-identities:${agent!.id}`, name: "GitHub identities", defaultAction: "deny", }).returning(); await db.insert(toolProfileEntries).values([personal!, dedicated!].map((connection) => ({ companyId: company!.id, profileId: profile!.id, selectorType: "connection" as const, effect: "include" as const, applicationId: application!.id, connectionId: connection.id, }))); await db.insert(toolProfileBindings).values({ companyId: company!.id, profileId: profile!.id, targetType: "agent", targetId: agent!.id, }); const [run] = await db.insert(heartbeatRuns).values({ companyId: company!.id, agentId: agent!.id, status: "running", responsibleUserId: "responsible-user", contextSnapshot: {}, }).returning(); const servers = await buildPaperclipRuntimeMcpServers({ db, agent: agent!, runId: run!.id }); expect(servers).toHaveLength(1); const [runtimeGateway] = await db.select().from(toolMcpGateways); expect(runtimeGateway).toBeTruthy(); const runtimeEntries = await db.select().from(toolProfileEntries) .where(eq(toolProfileEntries.profileId, runtimeGateway!.profileId!)); expect(runtimeEntries.map((entry) => entry.connectionId)).toEqual([dedicated!.id]); }); it("audits permitted remote MCP connections that were not installed when delivery is empty", async () => { const [company] = await db.insert(companies).values({ name: `Runtime MCP diagnostic ${randomUUID()}`, issuePrefix: `RD${randomUUID().slice(0, 5).toUpperCase()}`, }).returning(); const [agent] = await db.insert(agents).values({ companyId: company!.id, name: "Runtime MCP Diagnostic Agent", role: "engineer", adapterType: "codex_local", adapterConfig: {}, }).returning(); const [application] = await db.insert(toolApplications).values({ companyId: company!.id, applicationKey: `runtime-diagnostic-${randomUUID().slice(0, 8)}`, name: "Zapier", type: "mcp_http", status: "active", }).returning(); const [connection] = await db.insert(toolConnections).values({ companyId: company!.id, applicationId: application!.id, name: "Zapier", uid: `test/${randomUUID()}`, transport: "mcp_remote", status: "active", enabled: true, config: { url: "https://zapier.example.test/mcp" }, }).returning(); const [profile] = await db.insert(toolProfiles).values({ companyId: company!.id, profileKey: `app:${connection!.id}`, name: "Zapier", defaultAction: "deny", }).returning(); await db.insert(toolProfileEntries).values({ companyId: company!.id, profileId: profile!.id, selectorType: "connection", effect: "include", applicationId: application!.id, connectionId: connection!.id, }); await db.insert(toolProfileBindings).values({ companyId: company!.id, profileId: profile!.id, targetType: "agent", targetId: agent!.id, }); const runId = randomUUID(); await db.insert(heartbeatRuns).values({ id: runId, companyId: company!.id, agentId: agent!.id, status: "running", contextSnapshot: {}, }); const servers = await buildPaperclipRuntimeMcpServers({ db, agent: agent!, runId }); expect(servers).toEqual([]); const [activity] = await db .select() .from(activityLog) .where(eq(activityLog.action, "tool_gateway.runtime_mcp_delivery")); expect(activity).toMatchObject({ companyId: company!.id, agentId: agent!.id, runId, details: expect.objectContaining({ reasonCode: "permitted_connections_not_installed", deliveredServerCount: 0, permittedNotInstalledCount: 1, permittedNotInstalledConnections: [{ id: connection!.id, name: "Zapier" }], }), }); const [audit] = await db.select().from(toolAccessAuditEvents); expect(audit).toMatchObject({ companyId: company!.id, actorType: "agent", actorId: agent!.id, reasonCode: "permitted_connections_not_installed", details: expect.objectContaining({ runId, deliveredServerCount: 0 }), }); }); it("injects only managed gateways whose profile connections are installed for the agent", async () => { const [company] = await db.insert(companies).values({ name: `Managed gateway installs ${randomUUID()}`, issuePrefix: `MG${randomUUID().slice(0, 5).toUpperCase()}`, }).returning(); const [agent] = await db.insert(agents).values({ companyId: company!.id, name: "Managed Gateway Agent", role: "engineer", adapterType: "codex_local", adapterConfig: {}, }).returning(); const [application] = await db.insert(toolApplications).values({ companyId: company!.id, applicationKey: `managed-gateway-${randomUUID().slice(0, 8)}`, name: "Managed Gateway App", type: "mcp_http", status: "active", }).returning(); const connections = await db.insert(toolConnections).values([ { companyId: company!.id, applicationId: application!.id, name: "Installed gateway connection", uid: `test/${randomUUID()}`, transport: "mcp_remote", status: "active", enabled: true, }, { companyId: company!.id, applicationId: application!.id, name: "Uninstalled gateway connection", uid: `test/${randomUUID()}`, transport: "mcp_remote", status: "active", enabled: true, }, ]).returning(); const profiles = await db.insert(toolProfiles).values(connections.map((connection) => ({ companyId: company!.id, profileKey: `gateway:${connection.id}`, name: connection.name, defaultAction: "deny" as const, }))).returning(); await db.insert(toolProfileEntries).values(profiles.map((profile, index) => ({ companyId: company!.id, profileId: profile.id, selectorType: "connection" as const, effect: "include" as const, connectionId: connections[index]!.id, }))); const gateways = await db.insert(toolMcpGateways).values(profiles.map((profile, index) => ({ companyId: company!.id, name: `${connections[index]!.name} gateway`, slug: `gateway-${index}-${randomUUID().slice(0, 8)}`, profileId: profile.id, status: "active" as const, }))).returning(); await db.insert(toolConnectionInstalls).values({ companyId: company!.id, connectionId: connections[0]!.id, targetType: "agent", targetId: agent!.id, }); const config = await createManagedMcpRunConfig({ db, agent: agent!, runId: randomUUID(), config: {}, projectId: null, issueId: null, }); expect(config?.gateways).toHaveLength(1); expect(config?.gateways[0]).toMatchObject({ id: gateways[0]!.id, name: gateways[0]!.name, endpointPath: `/mcp/gateways/${gateways[0]!.gatewayPublicId}`, }); expect(config?.gateways.some((gateway) => gateway.id === gateways[1]!.id)).toBe(false); }); });