From a6e8f4d5862e2867e60558e4e08cd3e425eaa3fd Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Wed, 7 Jan 2026 19:56:47 -0800 Subject: [PATCH 01/14] feat(permission-groups): integration/model access controls for enterprise --- .../[id]/members/bulk/route.ts | 153 ++++ .../permission-groups/[id]/members/route.ts | 208 +++++ .../app/api/permission-groups/[id]/route.ts | 187 +++++ apps/sim/app/api/permission-groups/route.ts | 168 ++++ .../app/api/permission-groups/user/route.ts | 61 ++ .../components/log-details/log-details.tsx | 18 +- .../user-input/hooks/use-mention-data.ts | 26 +- .../components/combobox/combobox.tsx | 44 +- .../components/tool-input/tool-input.tsx | 30 +- .../panel/components/toolbar/toolbar.tsx | 9 +- .../access-control/access-control.tsx | 778 ++++++++++++++++++ .../settings-modal/components/index.ts | 1 + .../settings-modal/settings-modal.tsx | 33 +- apps/sim/executor/execution/block-executor.ts | 8 +- .../executor/handlers/agent/agent-handler.ts | 7 + .../handlers/evaluator/evaluator-handler.ts | 4 + .../handlers/router/router-handler.ts | 5 + apps/sim/executor/utils/permission-check.ts | 101 +++ apps/sim/hooks/queries/permission-groups.ts | 282 +++++++ apps/sim/hooks/use-permission-config.ts | 71 ++ apps/sim/lib/copilot/process-contents.ts | 22 +- .../tools/server/blocks/get-block-config.ts | 16 +- .../tools/server/blocks/get-block-options.ts | 13 +- .../server/blocks/get-blocks-and-tools.ts | 12 +- .../server/blocks/get-blocks-metadata-tool.ts | 18 +- .../tools/server/blocks/get-trigger-blocks.ts | 7 +- .../tools/server/workflow/edit-workflow.ts | 56 +- apps/sim/lib/permission-groups/types.ts | 25 + packages/db/schema.ts | 49 ++ 29 files changed, 2363 insertions(+), 49 deletions(-) create mode 100644 apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts create mode 100644 apps/sim/app/api/permission-groups/[id]/members/route.ts create mode 100644 apps/sim/app/api/permission-groups/[id]/route.ts create mode 100644 apps/sim/app/api/permission-groups/route.ts create mode 100644 apps/sim/app/api/permission-groups/user/route.ts create mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx create mode 100644 apps/sim/executor/utils/permission-check.ts create mode 100644 apps/sim/hooks/queries/permission-groups.ts create mode 100644 apps/sim/hooks/use-permission-config.ts create mode 100644 apps/sim/lib/permission-groups/types.ts diff --git a/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts b/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts new file mode 100644 index 0000000000..de71b405a5 --- /dev/null +++ b/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts @@ -0,0 +1,153 @@ +import { db } from '@sim/db' +import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq, inArray, notInArray } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' + +const logger = createLogger('PermissionGroupBulkMembers') + +async function getPermissionGroupWithAccess(groupId: string, userId: string) { + const [group] = await db + .select({ + id: permissionGroup.id, + organizationId: permissionGroup.organizationId, + }) + .from(permissionGroup) + .where(eq(permissionGroup.id, groupId)) + .limit(1) + + if (!group) return null + + const [membership] = await db + .select({ role: member.role }) + .from(member) + .where(and(eq(member.userId, userId), eq(member.organizationId, group.organizationId))) + .limit(1) + + if (!membership) return null + + return { group, role: membership.role } +} + +const bulkAddSchema = z.object({ + userIds: z.array(z.string()).optional(), + addAllOrgMembers: z.boolean().optional(), +}) + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const result = await getPermissionGroupWithAccess(id, session.user.id) + + if (!result) { + return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) + } + + if (result.role !== 'admin' && result.role !== 'owner') { + return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) + } + + const body = await req.json() + const { userIds, addAllOrgMembers } = bulkAddSchema.parse(body) + + let targetUserIds: string[] = [] + + if (addAllOrgMembers) { + const orgMembers = await db + .select({ userId: member.userId }) + .from(member) + .where(eq(member.organizationId, result.group.organizationId)) + + targetUserIds = orgMembers.map((m) => m.userId) + } else if (userIds && userIds.length > 0) { + const validMembers = await db + .select({ userId: member.userId }) + .from(member) + .where( + and( + eq(member.organizationId, result.group.organizationId), + inArray(member.userId, userIds) + ) + ) + + targetUserIds = validMembers.map((m) => m.userId) + } + + if (targetUserIds.length === 0) { + return NextResponse.json({ added: 0, moved: 0 }) + } + + const existingInThisGroup = await db + .select({ userId: permissionGroupMember.userId }) + .from(permissionGroupMember) + .where( + and( + eq(permissionGroupMember.permissionGroupId, id), + inArray(permissionGroupMember.userId, targetUserIds) + ) + ) + + const existingUserIds = new Set(existingInThisGroup.map((m) => m.userId)) + const usersToAdd = targetUserIds.filter((uid) => !existingUserIds.has(uid)) + + if (usersToAdd.length === 0) { + return NextResponse.json({ added: 0, moved: 0 }) + } + + const otherGroupMemberships = await db + .select({ + id: permissionGroupMember.id, + userId: permissionGroupMember.userId, + }) + .from(permissionGroupMember) + .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) + .where( + and( + eq(permissionGroup.organizationId, result.group.organizationId), + inArray(permissionGroupMember.userId, usersToAdd), + notInArray(permissionGroupMember.permissionGroupId, [id]) + ) + ) + + const movedCount = otherGroupMemberships.length + + if (otherGroupMemberships.length > 0) { + const idsToDelete = otherGroupMemberships.map((m) => m.id) + await db.delete(permissionGroupMember).where(inArray(permissionGroupMember.id, idsToDelete)) + } + + const newMembers = usersToAdd.map((userId) => ({ + id: crypto.randomUUID(), + permissionGroupId: id, + userId, + assignedBy: session.user.id, + assignedAt: new Date(), + })) + + await db.insert(permissionGroupMember).values(newMembers) + + logger.info('Bulk added members to permission group', { + permissionGroupId: id, + addedCount: usersToAdd.length, + movedCount, + assignedBy: session.user.id, + }) + + return NextResponse.json({ added: usersToAdd.length, moved: movedCount }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) + } + logger.error('Error bulk adding members to permission group', error) + return NextResponse.json({ error: 'Failed to add members' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/permission-groups/[id]/members/route.ts b/apps/sim/app/api/permission-groups/[id]/members/route.ts new file mode 100644 index 0000000000..f17916e2f0 --- /dev/null +++ b/apps/sim/app/api/permission-groups/[id]/members/route.ts @@ -0,0 +1,208 @@ +import { db } from '@sim/db' +import { member, permissionGroup, permissionGroupMember, user } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' + +const logger = createLogger('PermissionGroupMembers') + +async function getPermissionGroupWithAccess(groupId: string, userId: string) { + const [group] = await db + .select({ + id: permissionGroup.id, + organizationId: permissionGroup.organizationId, + }) + .from(permissionGroup) + .where(eq(permissionGroup.id, groupId)) + .limit(1) + + if (!group) return null + + const [membership] = await db + .select({ role: member.role }) + .from(member) + .where(and(eq(member.userId, userId), eq(member.organizationId, group.organizationId))) + .limit(1) + + if (!membership) return null + + return { group, role: membership.role } +} + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + const result = await getPermissionGroupWithAccess(id, session.user.id) + + if (!result) { + return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) + } + + const members = await db + .select({ + id: permissionGroupMember.id, + userId: permissionGroupMember.userId, + assignedAt: permissionGroupMember.assignedAt, + userName: user.name, + userEmail: user.email, + userImage: user.image, + }) + .from(permissionGroupMember) + .leftJoin(user, eq(permissionGroupMember.userId, user.id)) + .where(eq(permissionGroupMember.permissionGroupId, id)) + + return NextResponse.json({ members }) +} + +const addMemberSchema = z.object({ + userId: z.string().min(1), +}) + +export async function POST(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const result = await getPermissionGroupWithAccess(id, session.user.id) + + if (!result) { + return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) + } + + if (result.role !== 'admin' && result.role !== 'owner') { + return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) + } + + const body = await req.json() + const { userId } = addMemberSchema.parse(body) + + const [orgMember] = await db + .select({ id: member.id }) + .from(member) + .where(and(eq(member.userId, userId), eq(member.organizationId, result.group.organizationId))) + .limit(1) + + if (!orgMember) { + return NextResponse.json( + { error: 'User is not a member of this organization' }, + { status: 400 } + ) + } + + const existingMembership = await db + .select({ + id: permissionGroupMember.id, + permissionGroupId: permissionGroupMember.permissionGroupId, + }) + .from(permissionGroupMember) + .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) + .where( + and( + eq(permissionGroupMember.userId, userId), + eq(permissionGroup.organizationId, result.group.organizationId) + ) + ) + .limit(1) + + if (existingMembership.length > 0) { + if (existingMembership[0].permissionGroupId === id) { + return NextResponse.json( + { error: 'User is already in this permission group' }, + { status: 409 } + ) + } + await db + .delete(permissionGroupMember) + .where(eq(permissionGroupMember.id, existingMembership[0].id)) + } + + const newMember = { + id: crypto.randomUUID(), + permissionGroupId: id, + userId, + assignedBy: session.user.id, + assignedAt: new Date(), + } + + await db.insert(permissionGroupMember).values(newMember) + + logger.info('Added member to permission group', { + permissionGroupId: id, + userId, + assignedBy: session.user.id, + }) + + return NextResponse.json({ member: newMember }, { status: 201 }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) + } + logger.error('Error adding member to permission group', error) + return NextResponse.json({ error: 'Failed to add member' }, { status: 500 }) + } +} + +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + const { searchParams } = new URL(req.url) + const memberId = searchParams.get('memberId') + + if (!memberId) { + return NextResponse.json({ error: 'memberId is required' }, { status: 400 }) + } + + try { + const result = await getPermissionGroupWithAccess(id, session.user.id) + + if (!result) { + return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) + } + + if (result.role !== 'admin' && result.role !== 'owner') { + return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) + } + + const [memberToRemove] = await db + .select() + .from(permissionGroupMember) + .where( + and(eq(permissionGroupMember.id, memberId), eq(permissionGroupMember.permissionGroupId, id)) + ) + .limit(1) + + if (!memberToRemove) { + return NextResponse.json({ error: 'Member not found' }, { status: 404 }) + } + + await db.delete(permissionGroupMember).where(eq(permissionGroupMember.id, memberId)) + + logger.info('Removed member from permission group', { + permissionGroupId: id, + memberId, + userId: session.user.id, + }) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error('Error removing member from permission group', error) + return NextResponse.json({ error: 'Failed to remove member' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/permission-groups/[id]/route.ts b/apps/sim/app/api/permission-groups/[id]/route.ts new file mode 100644 index 0000000000..b6344e5243 --- /dev/null +++ b/apps/sim/app/api/permission-groups/[id]/route.ts @@ -0,0 +1,187 @@ +import { db } from '@sim/db' +import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { type NextRequest, NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { + type PermissionGroupConfig, + parsePermissionGroupConfig, +} from '@/lib/permission-groups/types' + +const logger = createLogger('PermissionGroup') + +const configSchema = z.object({ + allowedIntegrations: z.array(z.string()).nullable().optional(), + allowedModelProviders: z.array(z.string()).nullable().optional(), + hideTraceSpans: z.boolean().optional(), +}) + +const updateSchema = z.object({ + name: z.string().trim().min(1).max(100).optional(), + description: z.string().max(500).nullable().optional(), + config: configSchema.optional(), +}) + +async function getPermissionGroupWithAccess(groupId: string, userId: string) { + const [group] = await db + .select({ + id: permissionGroup.id, + organizationId: permissionGroup.organizationId, + name: permissionGroup.name, + description: permissionGroup.description, + config: permissionGroup.config, + createdBy: permissionGroup.createdBy, + createdAt: permissionGroup.createdAt, + updatedAt: permissionGroup.updatedAt, + }) + .from(permissionGroup) + .where(eq(permissionGroup.id, groupId)) + .limit(1) + + if (!group) return null + + const [membership] = await db + .select({ role: member.role }) + .from(member) + .where(and(eq(member.userId, userId), eq(member.organizationId, group.organizationId))) + .limit(1) + + if (!membership) return null + + return { group, role: membership.role } +} + +export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + const result = await getPermissionGroupWithAccess(id, session.user.id) + + if (!result) { + return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) + } + + return NextResponse.json({ + permissionGroup: { + ...result.group, + config: parsePermissionGroupConfig(result.group.config), + }, + }) +} + +export async function PUT(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const result = await getPermissionGroupWithAccess(id, session.user.id) + + if (!result) { + return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) + } + + if (result.role !== 'admin' && result.role !== 'owner') { + return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) + } + + const body = await req.json() + const updates = updateSchema.parse(body) + + if (updates.name) { + const existingGroup = await db + .select({ id: permissionGroup.id }) + .from(permissionGroup) + .where( + and( + eq(permissionGroup.organizationId, result.group.organizationId), + eq(permissionGroup.name, updates.name) + ) + ) + .limit(1) + + if (existingGroup.length > 0 && existingGroup[0].id !== id) { + return NextResponse.json( + { error: 'A permission group with this name already exists' }, + { status: 409 } + ) + } + } + + const currentConfig = parsePermissionGroupConfig(result.group.config) + const newConfig: PermissionGroupConfig = updates.config + ? { ...currentConfig, ...updates.config } + : currentConfig + + await db + .update(permissionGroup) + .set({ + ...(updates.name !== undefined && { name: updates.name }), + ...(updates.description !== undefined && { description: updates.description }), + config: newConfig, + updatedAt: new Date(), + }) + .where(eq(permissionGroup.id, id)) + + const [updated] = await db + .select() + .from(permissionGroup) + .where(eq(permissionGroup.id, id)) + .limit(1) + + return NextResponse.json({ + permissionGroup: { + ...updated, + config: parsePermissionGroupConfig(updated.config), + }, + }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) + } + logger.error('Error updating permission group', error) + return NextResponse.json({ error: 'Failed to update permission group' }, { status: 500 }) + } +} + +export async function DELETE(req: NextRequest, { params }: { params: Promise<{ id: string }> }) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { id } = await params + + try { + const result = await getPermissionGroupWithAccess(id, session.user.id) + + if (!result) { + return NextResponse.json({ error: 'Permission group not found' }, { status: 404 }) + } + + if (result.role !== 'admin' && result.role !== 'owner') { + return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) + } + + await db.delete(permissionGroupMember).where(eq(permissionGroupMember.permissionGroupId, id)) + await db.delete(permissionGroup).where(eq(permissionGroup.id, id)) + + logger.info('Deleted permission group', { permissionGroupId: id, userId: session.user.id }) + + return NextResponse.json({ success: true }) + } catch (error) { + logger.error('Error deleting permission group', error) + return NextResponse.json({ error: 'Failed to delete permission group' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/permission-groups/route.ts b/apps/sim/app/api/permission-groups/route.ts new file mode 100644 index 0000000000..e528c959e9 --- /dev/null +++ b/apps/sim/app/api/permission-groups/route.ts @@ -0,0 +1,168 @@ +import { db } from '@sim/db' +import { member, organization, permissionGroup, permissionGroupMember, user } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, count, desc, eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' +import { z } from 'zod' +import { getSession } from '@/lib/auth' +import { + DEFAULT_PERMISSION_GROUP_CONFIG, + type PermissionGroupConfig, + parsePermissionGroupConfig, +} from '@/lib/permission-groups/types' + +const logger = createLogger('PermissionGroups') + +const configSchema = z.object({ + allowedIntegrations: z.array(z.string()).nullable().optional(), + allowedModelProviders: z.array(z.string()).nullable().optional(), + hideTraceSpans: z.boolean().optional(), +}) + +const createSchema = z.object({ + organizationId: z.string().min(1), + name: z.string().trim().min(1).max(100), + description: z.string().max(500).optional(), + config: configSchema.optional(), +}) + +export async function GET(req: Request) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(req.url) + const organizationId = searchParams.get('organizationId') + + if (!organizationId) { + return NextResponse.json({ error: 'organizationId is required' }, { status: 400 }) + } + + const membership = await db + .select({ id: member.id, role: member.role }) + .from(member) + .where(and(eq(member.userId, session.user.id), eq(member.organizationId, organizationId))) + .limit(1) + + if (membership.length === 0) { + return NextResponse.json({ error: 'Forbidden' }, { status: 403 }) + } + + const groups = await db + .select({ + id: permissionGroup.id, + name: permissionGroup.name, + description: permissionGroup.description, + config: permissionGroup.config, + createdBy: permissionGroup.createdBy, + createdAt: permissionGroup.createdAt, + updatedAt: permissionGroup.updatedAt, + creatorName: user.name, + creatorEmail: user.email, + }) + .from(permissionGroup) + .leftJoin(user, eq(permissionGroup.createdBy, user.id)) + .where(eq(permissionGroup.organizationId, organizationId)) + .orderBy(desc(permissionGroup.createdAt)) + + const groupsWithCounts = await Promise.all( + groups.map(async (group) => { + const [memberCount] = await db + .select({ count: count() }) + .from(permissionGroupMember) + .where(eq(permissionGroupMember.permissionGroupId, group.id)) + + return { + ...group, + config: parsePermissionGroupConfig(group.config), + memberCount: memberCount?.count ?? 0, + } + }) + ) + + return NextResponse.json({ permissionGroups: groupsWithCounts }) +} + +export async function POST(req: Request) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + try { + const body = await req.json() + const { organizationId, name, description, config } = createSchema.parse(body) + + const membership = await db + .select({ id: member.id, role: member.role }) + .from(member) + .where(and(eq(member.userId, session.user.id), eq(member.organizationId, organizationId))) + .limit(1) + + const role = membership[0]?.role + if (membership.length === 0 || (role !== 'admin' && role !== 'owner')) { + return NextResponse.json({ error: 'Admin or owner permissions required' }, { status: 403 }) + } + + const orgExists = await db + .select({ id: organization.id }) + .from(organization) + .where(eq(organization.id, organizationId)) + .limit(1) + + if (orgExists.length === 0) { + return NextResponse.json({ error: 'Organization not found' }, { status: 404 }) + } + + const existingGroup = await db + .select({ id: permissionGroup.id }) + .from(permissionGroup) + .where( + and(eq(permissionGroup.organizationId, organizationId), eq(permissionGroup.name, name)) + ) + .limit(1) + + if (existingGroup.length > 0) { + return NextResponse.json( + { error: 'A permission group with this name already exists' }, + { status: 409 } + ) + } + + const groupConfig: PermissionGroupConfig = { + ...DEFAULT_PERMISSION_GROUP_CONFIG, + ...config, + } + + const now = new Date() + const newGroup = { + id: crypto.randomUUID(), + organizationId, + name, + description: description || null, + config: groupConfig, + createdBy: session.user.id, + createdAt: now, + updatedAt: now, + } + + await db.insert(permissionGroup).values(newGroup) + + logger.info('Created permission group', { + permissionGroupId: newGroup.id, + organizationId, + userId: session.user.id, + }) + + return NextResponse.json({ permissionGroup: newGroup }, { status: 201 }) + } catch (error) { + if (error instanceof z.ZodError) { + return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) + } + logger.error('Error creating permission group', error) + return NextResponse.json({ error: 'Failed to create permission group' }, { status: 500 }) + } +} diff --git a/apps/sim/app/api/permission-groups/user/route.ts b/apps/sim/app/api/permission-groups/user/route.ts new file mode 100644 index 0000000000..10df06f239 --- /dev/null +++ b/apps/sim/app/api/permission-groups/user/route.ts @@ -0,0 +1,61 @@ +import { db } from '@sim/db' +import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' +import { and, eq } from 'drizzle-orm' +import { NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { parsePermissionGroupConfig } from '@/lib/permission-groups/types' + +export async function GET(req: Request) { + const session = await getSession() + + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(req.url) + const organizationId = searchParams.get('organizationId') + + if (!organizationId) { + return NextResponse.json({ error: 'organizationId is required' }, { status: 400 }) + } + + const [membership] = await db + .select({ id: member.id }) + .from(member) + .where(and(eq(member.userId, session.user.id), eq(member.organizationId, organizationId))) + .limit(1) + + if (!membership) { + return NextResponse.json({ error: 'Not a member of this organization' }, { status: 403 }) + } + + const [groupMembership] = await db + .select({ + permissionGroupId: permissionGroupMember.permissionGroupId, + config: permissionGroup.config, + groupName: permissionGroup.name, + }) + .from(permissionGroupMember) + .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) + .where( + and( + eq(permissionGroupMember.userId, session.user.id), + eq(permissionGroup.organizationId, organizationId) + ) + ) + .limit(1) + + if (!groupMembership) { + return NextResponse.json({ + permissionGroupId: null, + groupName: null, + config: null, + }) + } + + return NextResponse.json({ + permissionGroupId: groupMembership.permissionGroupId, + groupName: groupMembership.groupName, + config: parsePermissionGroupConfig(groupMembership.config), + }) +} diff --git a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx index 611f1b8976..8287c0f6a1 100644 --- a/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx +++ b/apps/sim/app/workspace/[workspaceId]/logs/components/log-details/log-details.tsx @@ -13,6 +13,7 @@ import { StatusBadge, TriggerBadge, } from '@/app/workspace/[workspaceId]/logs/utils' +import { usePermissionConfig } from '@/hooks/use-permission-config' import { formatCost } from '@/providers/utils' import type { WorkflowLog } from '@/stores/logs/filters/types' import { useLogDetailsUIStore } from '@/stores/logs/store' @@ -53,6 +54,7 @@ export const LogDetails = memo(function LogDetails({ const scrollAreaRef = useRef(null) const panelWidth = useLogDetailsUIStore((state) => state.panelWidth) const { handleMouseDown } = useLogDetailsResize() + const { config: permissionConfig } = usePermissionConfig() useEffect(() => { if (scrollAreaRef.current) { @@ -260,7 +262,7 @@ export const LogDetails = memo(function LogDetails({ {/* Workflow State */} - {isWorkflowExecutionLog && log.executionId && ( + {isWorkflowExecutionLog && log.executionId && !permissionConfig.hideTraceSpans && (
Workflow State @@ -278,12 +280,14 @@ export const LogDetails = memo(function LogDetails({ )} {/* Workflow Execution - Trace Spans */} - {isWorkflowExecutionLog && log.executionData?.traceSpans && ( - - )} + {isWorkflowExecutionLog && + log.executionData?.traceSpans && + !permissionConfig.hideTraceSpans && ( + + )} {/* Files */} {log.files && log.files.length > 0 && ( diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts index 79695526fb..476623e8b7 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/copilot/components/user-input/hooks/use-mention-data.ts @@ -3,6 +3,7 @@ import { useCallback, useEffect, useState } from 'react' import { createLogger } from '@sim/logger' import { useShallow } from 'zustand/react/shallow' +import { usePermissionConfig } from '@/hooks/use-permission-config' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useWorkflowStore } from '@/stores/workflows/workflow/store' @@ -92,6 +93,8 @@ interface UseMentionDataProps { export function useMentionData(props: UseMentionDataProps) { const { workflowId, workspaceId } = props + const { config, isBlockAllowed } = usePermissionConfig() + const [pastChats, setPastChats] = useState([]) const [isLoadingPastChats, setIsLoadingPastChats] = useState(false) @@ -101,6 +104,11 @@ export function useMentionData(props: UseMentionDataProps) { const [blocksList, setBlocksList] = useState([]) const [isLoadingBlocks, setIsLoadingBlocks] = useState(false) + // Reset blocks list when permission config changes + useEffect(() => { + setBlocksList([]) + }, [config.allowedIntegrations]) + const [templatesList, setTemplatesList] = useState([]) const [isLoadingTemplates, setIsLoadingTemplates] = useState(false) @@ -252,7 +260,13 @@ export function useMentionData(props: UseMentionDataProps) { const { getAllBlocks } = await import('@/blocks') const all = getAllBlocks() const regularBlocks = all - .filter((b: any) => b.type !== 'starter' && !b.hideFromToolbar && b.category === 'blocks') + .filter( + (b: any) => + b.type !== 'starter' && + !b.hideFromToolbar && + b.category === 'blocks' && + isBlockAllowed(b.type) + ) .map((b: any) => ({ id: b.type, name: b.name || b.type, @@ -262,7 +276,13 @@ export function useMentionData(props: UseMentionDataProps) { .sort((a: any, b: any) => a.name.localeCompare(b.name)) const toolBlocks = all - .filter((b: any) => b.type !== 'starter' && !b.hideFromToolbar && b.category === 'tools') + .filter( + (b: any) => + b.type !== 'starter' && + !b.hideFromToolbar && + b.category === 'tools' && + isBlockAllowed(b.type) + ) .map((b: any) => ({ id: b.type, name: b.name || b.type, @@ -276,7 +296,7 @@ export function useMentionData(props: UseMentionDataProps) { } finally { setIsLoadingBlocks(false) } - }, [isLoadingBlocks, blocksList.length]) + }, [isLoadingBlocks, blocksList.length, isBlockAllowed]) /** * Ensures templates are loaded diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx index c5b8f67e2e..31cb326ad8 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx @@ -8,6 +8,8 @@ import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/c import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes' import type { SubBlockConfig } from '@/blocks/types' import { getDependsOnFields } from '@/blocks/utils' +import { usePermissionConfig } from '@/hooks/use-permission-config' +import { getProviderFromModel } from '@/providers/utils' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -132,10 +134,27 @@ export function ComboBox({ // Determine the active value based on mode (preview vs. controlled vs. store) const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue + // Permission-based filtering for model dropdowns + const { isProviderAllowed } = usePermissionConfig() + // Evaluate static options if provided as a function const staticOptions = useMemo(() => { - return typeof options === 'function' ? options() : options - }, [options]) + const opts = typeof options === 'function' ? options() : options + + if (subBlockId === 'model') { + return opts.filter((opt) => { + const modelId = typeof opt === 'string' ? opt : opt.id + try { + const providerId = getProviderFromModel(modelId) + return isProviderAllowed(providerId) + } catch { + return true + } + }) + } + + return opts + }, [options, subBlockId, isProviderAllowed]) // Normalize fetched options to match ComboBoxOption format const normalizedFetchedOptions = useMemo((): ComboBoxOption[] => { @@ -147,6 +166,18 @@ export function ComboBox({ let opts: ComboBoxOption[] = fetchOptions && normalizedFetchedOptions.length > 0 ? normalizedFetchedOptions : staticOptions + if (subBlockId === 'model' && fetchOptions && normalizedFetchedOptions.length > 0) { + opts = opts.filter((opt) => { + const modelId = typeof opt === 'string' ? opt : opt.id + try { + const providerId = getProviderFromModel(modelId) + return isProviderAllowed(providerId) + } catch { + return true + } + }) + } + // Merge hydrated option if not already present if (hydratedOption) { const alreadyPresent = opts.some((o) => @@ -158,7 +189,14 @@ export function ComboBox({ } return opts - }, [fetchOptions, normalizedFetchedOptions, staticOptions, hydratedOption]) + }, [ + fetchOptions, + normalizedFetchedOptions, + staticOptions, + hydratedOption, + subBlockId, + isProviderAllowed, + ]) // Convert options to Combobox format const comboboxOptions = useMemo((): ComboboxOption[] => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 6823e303b4..03b37d955d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -59,6 +59,7 @@ import { import { useForceRefreshMcpTools, useMcpServers, useStoredMcpTools } from '@/hooks/queries/mcp' import { useWorkflows } from '@/hooks/queries/workflows' import { useMcpTools } from '@/hooks/use-mcp-tools' +import { usePermissionConfig } from '@/hooks/use-permission-config' import { getProviderFromModel, supportsToolUsageControl } from '@/providers/utils' import { useSettingsModalStore } from '@/stores/settings-modal/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' @@ -1003,18 +1004,23 @@ export function ToolInput({ const provider = model ? getProviderFromModel(model) : '' const supportsToolControl = provider ? supportsToolUsageControl(provider) : false - const toolBlocks = getAllBlocks().filter( - (block) => - (block.category === 'tools' || - block.type === 'api' || - block.type === 'webhook_request' || - block.type === 'workflow' || - block.type === 'knowledge' || - block.type === 'function') && - block.type !== 'evaluator' && - block.type !== 'mcp' && - block.type !== 'file' - ) + const { filterBlocks } = usePermissionConfig() + + const toolBlocks = useMemo(() => { + const allToolBlocks = getAllBlocks().filter( + (block) => + (block.category === 'tools' || + block.type === 'api' || + block.type === 'webhook_request' || + block.type === 'workflow' || + block.type === 'knowledge' || + block.type === 'function') && + block.type !== 'evaluator' && + block.type !== 'mcp' && + block.type !== 'file' + ) + return filterBlocks(allToolBlocks) + }, [filterBlocks]) const customFilter = useCallback((value: string, search: string) => { if (!search.trim()) return 1 diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx index 303067dd92..4ebc1a73fa 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx @@ -25,6 +25,7 @@ import { import { LoopTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/loop/loop-config' import { ParallelTool } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/subflows/parallel/parallel-config' import type { BlockConfig } from '@/blocks/types' +import { usePermissionConfig } from '@/hooks/use-permission-config' import { useToolbarStore } from '@/stores/panel/toolbar/store' interface BlockItem { @@ -192,9 +193,15 @@ export const Toolbar = forwardRef(function Toolbar( triggersHeaderRef, }) + // Permission config for filtering + const { filterBlocks } = usePermissionConfig() + // Get static data (computed once and cached) const triggers = getTriggers() - const blocks = getBlocks() + const allBlocks = getBlocks() + + // Apply permission-based filtering to blocks + const blocks = useMemo(() => filterBlocks(allBlocks), [filterBlocks, allBlocks]) // Determine if triggers are at minimum height (blocks are fully expanded) const isTriggersAtMinimum = toolbarTriggersHeight <= TRIGGERS_MIN_THRESHOLD diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx new file mode 100644 index 0000000000..7367be2701 --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx @@ -0,0 +1,778 @@ +'use client' + +import { useCallback, useMemo, useState } from 'react' +import { createLogger } from '@sim/logger' +import { Check, Plus, Search, Users } from 'lucide-react' +import { + Avatar, + AvatarFallback, + AvatarImage, + Button, + Checkbox, + Input, + Label, + Modal, + ModalBody, + ModalContent, + ModalFooter, + ModalHeader, +} from '@/components/emcn' +import { Input as BaseInput, Skeleton } from '@/components/ui' +import { useSession } from '@/lib/auth/auth-client' +import { getSubscriptionStatus } from '@/lib/billing/client' +import { cn } from '@/lib/core/utils/cn' +import type { PermissionGroupConfig } from '@/lib/permission-groups/types' +import { getUserRole } from '@/lib/workspaces/organization' +import { getUserColor } from '@/app/workspace/[workspaceId]/w/utils/get-user-color' +import { getAllBlocks } from '@/blocks' +import { useOrganization, useOrganizations } from '@/hooks/queries/organization' +import { + type PermissionGroup, + useBulkAddPermissionGroupMembers, + useCreatePermissionGroup, + useDeletePermissionGroup, + usePermissionGroupMembers, + usePermissionGroups, + useRemovePermissionGroupMember, + useUpdatePermissionGroup, +} from '@/hooks/queries/permission-groups' +import { useSubscriptionData } from '@/hooks/queries/subscription' +import { getAllProviderIds } from '@/providers/utils' + +const logger = createLogger('AccessControl') + +function AccessControlSkeleton() { + return ( +
+
+ +
+
+ +
+ + +
+
+ +
+
+
+ ) +} + +export function AccessControl() { + const { data: session } = useSession() + const { data: organizationsData, isPending: orgsLoading } = useOrganizations() + const { data: subscriptionData, isPending: subLoading } = useSubscriptionData() + + const activeOrganization = organizationsData?.activeOrganization + const subscriptionStatus = getSubscriptionStatus(subscriptionData?.data) + const hasEnterprisePlan = subscriptionStatus.isEnterprise + const userRole = getUserRole(activeOrganization, session?.user?.email) + const isOwner = userRole === 'owner' + const canManage = hasEnterprisePlan && isOwner && !!activeOrganization?.id + + const queryEnabled = !!activeOrganization?.id + const { data: permissionGroups = [], isPending: groupsLoading } = usePermissionGroups( + activeOrganization?.id, + queryEnabled + ) + + // Show loading while dependencies load, or while permission groups query is pending + const isLoading = orgsLoading || subLoading || (queryEnabled && groupsLoading) + const { data: organization } = useOrganization(activeOrganization?.id || '') + + const createPermissionGroup = useCreatePermissionGroup() + const updatePermissionGroup = useUpdatePermissionGroup() + const deletePermissionGroup = useDeletePermissionGroup() + const bulkAddMembers = useBulkAddPermissionGroupMembers() + + const [searchTerm, setSearchTerm] = useState('') + const [showCreateModal, setShowCreateModal] = useState(false) + const [viewingGroup, setViewingGroup] = useState(null) + const [newGroupName, setNewGroupName] = useState('') + const [newGroupDescription, setNewGroupDescription] = useState('') + const [createError, setCreateError] = useState(null) + const [deletingGroup, setDeletingGroup] = useState<{ id: string; name: string } | null>(null) + const [deletingGroupIds, setDeletingGroupIds] = useState>(new Set()) + + const { data: members = [], isPending: membersLoading } = usePermissionGroupMembers( + viewingGroup?.id + ) + const removeMember = useRemovePermissionGroupMember() + + const [showConfigModal, setShowConfigModal] = useState(false) + const [editingConfig, setEditingConfig] = useState(null) + const [showAddMembersModal, setShowAddMembersModal] = useState(false) + const [selectedMemberIds, setSelectedMemberIds] = useState>(new Set()) + + const allBlocks = useMemo(() => getAllBlocks().filter((b) => !b.hideFromToolbar), []) + const allProviderIds = useMemo(() => getAllProviderIds(), []) + + const orgMembers = useMemo(() => { + return organization?.members || [] + }, [organization]) + + const filteredGroups = useMemo(() => { + if (!searchTerm.trim()) return permissionGroups + const searchLower = searchTerm.toLowerCase() + return permissionGroups.filter((g) => g.name.toLowerCase().includes(searchLower)) + }, [permissionGroups, searchTerm]) + + const handleCreatePermissionGroup = useCallback(async () => { + if (!newGroupName.trim() || !activeOrganization?.id) return + setCreateError(null) + try { + const result = await createPermissionGroup.mutateAsync({ + organizationId: activeOrganization.id, + name: newGroupName.trim(), + description: newGroupDescription.trim() || undefined, + }) + setShowCreateModal(false) + setNewGroupName('') + setNewGroupDescription('') + + if (result?.permissionGroup) { + setViewingGroup(result.permissionGroup) + } + } catch (error) { + logger.error('Failed to create permission group', error) + if (error instanceof Error) { + setCreateError(error.message) + } else { + setCreateError('Failed to create permission group') + } + } + }, [newGroupName, newGroupDescription, activeOrganization?.id, createPermissionGroup]) + + const handleCloseCreateModal = useCallback(() => { + setShowCreateModal(false) + setNewGroupName('') + setNewGroupDescription('') + setCreateError(null) + }, []) + + const handleBackToList = useCallback(() => { + setViewingGroup(null) + }, []) + + const handleDeleteClick = useCallback((group: PermissionGroup) => { + setDeletingGroup({ id: group.id, name: group.name }) + }, []) + + const confirmDelete = useCallback(async () => { + if (!deletingGroup || !activeOrganization?.id) return + setDeletingGroupIds((prev) => new Set(prev).add(deletingGroup.id)) + try { + await deletePermissionGroup.mutateAsync({ + permissionGroupId: deletingGroup.id, + organizationId: activeOrganization.id, + }) + setDeletingGroup(null) + if (viewingGroup?.id === deletingGroup.id) { + setViewingGroup(null) + } + } catch (error) { + logger.error('Failed to delete permission group', error) + } finally { + setDeletingGroupIds((prev) => { + const next = new Set(prev) + next.delete(deletingGroup.id) + return next + }) + } + }, [deletingGroup, activeOrganization?.id, deletePermissionGroup, viewingGroup?.id]) + + const handleRemoveMember = useCallback( + async (memberId: string) => { + if (!viewingGroup) return + try { + await removeMember.mutateAsync({ + permissionGroupId: viewingGroup.id, + memberId, + }) + } catch (error) { + logger.error('Failed to remove member', error) + } + }, + [viewingGroup, removeMember] + ) + + const handleOpenConfigModal = useCallback(() => { + if (!viewingGroup) return + setEditingConfig({ ...viewingGroup.config }) + setShowConfigModal(true) + }, [viewingGroup]) + + const handleSaveConfig = useCallback(async () => { + if (!viewingGroup || !editingConfig || !activeOrganization?.id) return + try { + await updatePermissionGroup.mutateAsync({ + id: viewingGroup.id, + organizationId: activeOrganization.id, + config: editingConfig, + }) + setShowConfigModal(false) + setEditingConfig(null) + setViewingGroup((prev) => (prev ? { ...prev, config: editingConfig } : null)) + } catch (error) { + logger.error('Failed to update config', error) + } + }, [viewingGroup, editingConfig, activeOrganization?.id, updatePermissionGroup]) + + const handleOpenAddMembersModal = useCallback(() => { + const existingMemberUserIds = new Set(members.map((m) => m.userId)) + setSelectedMemberIds(new Set()) + setShowAddMembersModal(true) + }, [members]) + + const handleAddSelectedMembers = useCallback(async () => { + if (!viewingGroup || selectedMemberIds.size === 0) return + try { + await bulkAddMembers.mutateAsync({ + permissionGroupId: viewingGroup.id, + userIds: Array.from(selectedMemberIds), + }) + setShowAddMembersModal(false) + setSelectedMemberIds(new Set()) + } catch (error) { + logger.error('Failed to add members', error) + } + }, [viewingGroup, selectedMemberIds, bulkAddMembers]) + + const toggleIntegration = useCallback( + (blockType: string) => { + if (!editingConfig) return + const current = editingConfig.allowedIntegrations + if (current === null) { + const allExcept = allBlocks.map((b) => b.type).filter((t) => t !== blockType) + setEditingConfig({ ...editingConfig, allowedIntegrations: allExcept }) + } else if (current.includes(blockType)) { + const updated = current.filter((t) => t !== blockType) + setEditingConfig({ + ...editingConfig, + allowedIntegrations: updated.length === allBlocks.length ? null : updated, + }) + } else { + const updated = [...current, blockType] + setEditingConfig({ + ...editingConfig, + allowedIntegrations: updated.length === allBlocks.length ? null : updated, + }) + } + }, + [editingConfig, allBlocks] + ) + + const toggleProvider = useCallback( + (providerId: string) => { + if (!editingConfig) return + const current = editingConfig.allowedModelProviders + if (current === null) { + const allExcept = allProviderIds.filter((p) => p !== providerId) + setEditingConfig({ ...editingConfig, allowedModelProviders: allExcept }) + } else if (current.includes(providerId)) { + const updated = current.filter((p) => p !== providerId) + setEditingConfig({ + ...editingConfig, + allowedModelProviders: updated.length === allProviderIds.length ? null : updated, + }) + } else { + const updated = [...current, providerId] + setEditingConfig({ + ...editingConfig, + allowedModelProviders: updated.length === allProviderIds.length ? null : updated, + }) + } + }, + [editingConfig, allProviderIds] + ) + + const isIntegrationAllowed = useCallback( + (blockType: string) => { + if (!editingConfig) return true + return ( + editingConfig.allowedIntegrations === null || + editingConfig.allowedIntegrations.includes(blockType) + ) + }, + [editingConfig] + ) + + const isProviderAllowed = useCallback( + (providerId: string) => { + if (!editingConfig) return true + return ( + editingConfig.allowedModelProviders === null || + editingConfig.allowedModelProviders.includes(providerId) + ) + }, + [editingConfig] + ) + + const availableMembersToAdd = useMemo(() => { + const existingMemberUserIds = new Set(members.map((m) => m.userId)) + return orgMembers.filter((m: any) => !existingMemberUserIds.has(m.userId)) + }, [orgMembers, members]) + + if (isLoading) { + return + } + + if (viewingGroup) { + return ( + <> +
+
+
+
+
+
+ + Group Name + + + {viewingGroup.name} + +
+ {viewingGroup.description && ( + <> +
+ + {viewingGroup.description} + + + )} +
+ +
+ +
+
+

Members

+ +
+ + {membersLoading ? ( +
+ {[1, 2].map((i) => ( +
+
+ +
+ + +
+
+
+ ))} +
+ ) : members.length === 0 ? ( +

+ No members yet. Add members using the buttons above. +

+ ) : ( +
+ {members.map((member) => { + const name = member.userName || 'Unknown' + const avatarInitial = name.charAt(0).toUpperCase() + + return ( +
+
+ + {member.userImage && ( + + )} + + {avatarInitial} + + + +
+
+ + {name} + +
+
+ {member.userEmail} +
+
+
+ +
+ +
+
+ ) + })} +
+ )} +
+
+
+ +
+ +
+
+ + + + Configure Permissions + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, hideTraceSpans: checked === true } : prev + ) + } + /> + +
+ +
+ +

+ Select which model providers are available in agent dropdowns. All are allowed + by default. +

+
+ {allProviderIds.map((providerId) => ( + + ))} +
+
+ +
+ +

+ Select which integrations are visible in the toolbar. All are visible by + default. +

+
+ {allBlocks.map((block) => ( + + ))} +
+
+
+
+ + + + +
+
+ + + + Add Members + + {availableMembersToAdd.length === 0 ? ( +

+ All organization members are already in this group. +

+ ) : ( +
+ + {availableMembersToAdd.map((member: any) => { + const name = member.user?.name || 'Unknown' + const email = member.user?.email || '' + const avatarInitial = name.charAt(0).toUpperCase() + const isSelected = selectedMemberIds.has(member.userId) + + return ( + + ) + })} +
+ )} +
+ + + + +
+
+ + ) + } + + return ( + <> +
+
+
+ + setSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
+ +
+ +
+ {filteredGroups.length === 0 && searchTerm.trim() ? ( +
+ No results found matching "{searchTerm}" +
+ ) : permissionGroups.length === 0 ? ( +
+ No permission groups created yet +
+ ) : ( +
+ {filteredGroups.map((group) => ( +
+
+ {group.name} + + {group.memberCount} member{group.memberCount !== 1 ? 's' : ''} + +
+
+ + +
+
+ ))} +
+ )} +
+
+ + + + Create Permission Group + +
+
+ + { + setNewGroupName(e.target.value) + if (createError) setCreateError(null) + }} + placeholder='e.g., Marketing Team' + /> +
+
+ + setNewGroupDescription(e.target.value)} + placeholder='e.g., Limited access for marketing users' + /> +
+ {createError &&

{createError}

} +
+
+ + + + +
+
+ + setDeletingGroup(null)}> + + Delete Permission Group + +

+ Are you sure you want to delete{' '} + {deletingGroup?.name}? + All members will be removed from this group.{' '} + This action cannot be undone. +

+
+ + + + +
+
+ + ) +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts index af86138a71..f83fd34de6 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/index.ts @@ -1,3 +1,4 @@ +export { AccessControl } from './access-control/access-control' export { ApiKeys } from './api-keys/api-keys' export { BYOK } from './byok/byok' export { Copilot } from './copilot/copilot' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx index 9fcced4690..c960c7f32c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx @@ -4,7 +4,18 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import * as DialogPrimitive from '@radix-ui/react-dialog' import * as VisuallyHidden from '@radix-ui/react-visually-hidden' import { useQueryClient } from '@tanstack/react-query' -import { Files, KeySquare, LogIn, Mail, Server, Settings, User, Users, Wrench } from 'lucide-react' +import { + Files, + KeySquare, + LogIn, + Mail, + Server, + Settings, + ShieldCheck, + User, + Users, + Wrench, +} from 'lucide-react' import { Card, Connections, @@ -29,6 +40,7 @@ import { getEnv, isTruthy } from '@/lib/core/config/env' import { isHosted } from '@/lib/core/config/feature-flags' import { getUserRole } from '@/lib/workspaces/organization' import { + AccessControl, ApiKeys, BYOK, Copilot, @@ -65,6 +77,7 @@ type SettingsSection = | 'template-profile' | 'integrations' | 'credential-sets' + | 'access-control' | 'apikeys' | 'byok' | 'files' @@ -100,6 +113,15 @@ const sectionConfig: { key: NavigationSection; title: string }[] = [ const allNavigationItems: NavigationItem[] = [ { id: 'general', label: 'General', icon: Settings, section: 'account' }, { id: 'template-profile', label: 'Template Profile', icon: User, section: 'account' }, + { + id: 'access-control', + label: 'Access Control', + icon: ShieldCheck, + section: 'account', + requiresTeam: true, + requiresEnterprise: true, + requiresOwner: true, + }, { id: 'subscription', label: 'Subscription', @@ -204,6 +226,10 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { return !hasProviders || isSSOProviderOwner === true } + if (item.requiresEnterprise && !hasEnterprisePlan) { + return false + } + if (item.requiresTeam) { const isMember = userRole === 'member' || isAdmin const hasTeamPlan = subscriptionStatus.isTeam || subscriptionStatus.isEnterprise @@ -214,10 +240,6 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { return false } - if (item.requiresEnterprise && !hasEnterprisePlan) { - return false - } - if (item.requiresHosted && !isHosted) { return false } @@ -472,6 +494,7 @@ export function SettingsModal({ open, onOpenChange }: SettingsModalProps) { /> )} {activeSection === 'credential-sets' && } + {activeSection === 'access-control' && } {activeSection === 'apikeys' && } {activeSection === 'files' && } {isBillingEnabled && activeSection === 'subscription' && } diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 1860dbc9fc..b4e56b5d15 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -26,6 +26,7 @@ import type { } from '@/executor/types' import { streamingResponseFormatProcessor } from '@/executor/utils' import { buildBlockExecutionError, normalizeError } from '@/executor/utils/errors' +import { validateBlockType } from '@/executor/utils/permission-check' import type { VariableResolver } from '@/executor/variables/resolver' import type { SerializedBlock } from '@/serializer/types' import type { SubflowType } from '@/stores/workflows/workflow/types' @@ -54,7 +55,8 @@ export class BlockExecutor { }) } - const isSentinel = isSentinelBlockType(block.metadata?.id ?? '') + const blockType = block.metadata?.id ?? '' + const isSentinel = isSentinelBlockType(blockType) let blockLog: BlockLog | undefined if (!isSentinel) { @@ -74,6 +76,10 @@ export class BlockExecutor { } try { + if (!isSentinel && blockType) { + await validateBlockType(ctx.userId, blockType) + } + resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) if (block.metadata?.id === BlockType.AGENT && resolvedInputs.tools) { diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 392a99da9a..4fe3a6d1bc 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -25,6 +25,7 @@ import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/execu import { collectBlockData } from '@/executor/utils/block-data' import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http' import { stringifyJSON } from '@/executor/utils/json' +import { validateBlockType, validateModelProvider } from '@/executor/utils/permission-check' import { executeProviderRequest } from '@/providers' import { getProviderFromModel, transformBlockTool } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -52,6 +53,9 @@ export class AgentBlockHandler implements BlockHandler { const responseFormat = this.parseResponseFormat(filteredInputs.responseFormat) const model = filteredInputs.model || AGENT.DEFAULT_MODEL + + await validateModelProvider(ctx.userId, model) + const providerId = getProviderFromModel(model) const formattedTools = await this.formatTools(ctx, filteredInputs.tools || []) const streamingConfig = this.getStreamingConfig(ctx, block) @@ -212,6 +216,9 @@ export class AgentBlockHandler implements BlockHandler { const otherResults = await Promise.all( otherTools.map(async (tool) => { try { + if (tool.type && tool.type !== 'custom-tool' && tool.type !== 'mcp') { + await validateBlockType(ctx.userId, tool.type) + } if (tool.type === 'custom-tool' && (tool.schema || tool.customToolId)) { return await this.createCustomTool(ctx, tool) } diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index e7a768ee33..8578c07074 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -8,6 +8,7 @@ import { BlockType, DEFAULTS, EVALUATOR, HTTP } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' import { buildAPIUrl, extractAPIErrorMessage } from '@/executor/utils/http' import { isJSONString, parseJSON, stringifyJSON } from '@/executor/utils/json' +import { validateModelProvider } from '@/executor/utils/permission-check' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -33,6 +34,9 @@ export class EvaluatorBlockHandler implements BlockHandler { vertexLocation: inputs.vertexLocation, vertexCredential: inputs.vertexCredential, } + + await validateModelProvider(ctx.userId, evaluatorConfig.model) + const providerId = getProviderFromModel(evaluatorConfig.model) let finalApiKey: string | undefined = evaluatorConfig.apiKey diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index 55524b7050..352f0ecc7c 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -15,6 +15,7 @@ import { ROUTER, } from '@/executor/constants' import type { BlockHandler, ExecutionContext } from '@/executor/types' +import { validateModelProvider } from '@/executor/utils/permission-check' import { calculateCost, getProviderFromModel } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -70,6 +71,8 @@ export class RouterBlockHandler implements BlockHandler { vertexCredential: inputs.vertexCredential, } + await validateModelProvider(ctx.userId, routerConfig.model) + const providerId = getProviderFromModel(routerConfig.model) try { @@ -199,6 +202,8 @@ export class RouterBlockHandler implements BlockHandler { vertexCredential: inputs.vertexCredential, } + await validateModelProvider(ctx.userId, routerConfig.model) + const providerId = getProviderFromModel(routerConfig.model) try { diff --git a/apps/sim/executor/utils/permission-check.ts b/apps/sim/executor/utils/permission-check.ts new file mode 100644 index 0000000000..50eef90882 --- /dev/null +++ b/apps/sim/executor/utils/permission-check.ts @@ -0,0 +1,101 @@ +import { db } from '@sim/db' +import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { and, eq } from 'drizzle-orm' +import { + type PermissionGroupConfig, + parsePermissionGroupConfig, +} from '@/lib/permission-groups/types' +import { getProviderFromModel } from '@/providers/utils' + +const logger = createLogger('PermissionCheck') + +export class ProviderNotAllowedError extends Error { + constructor(providerId: string, model: string) { + super( + `Provider "${providerId}" is not allowed for model "${model}" based on your permission group settings` + ) + this.name = 'ProviderNotAllowedError' + } +} + +export class IntegrationNotAllowedError extends Error { + constructor(blockType: string) { + super(`Integration "${blockType}" is not allowed based on your permission group settings`) + this.name = 'IntegrationNotAllowedError' + } +} + +export async function getUserPermissionConfig( + userId: string +): Promise { + const [membership] = await db + .select({ organizationId: member.organizationId }) + .from(member) + .where(eq(member.userId, userId)) + .limit(1) + + if (!membership) { + return null + } + + const [groupMembership] = await db + .select({ config: permissionGroup.config }) + .from(permissionGroupMember) + .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) + .where( + and( + eq(permissionGroupMember.userId, userId), + eq(permissionGroup.organizationId, membership.organizationId) + ) + ) + .limit(1) + + if (!groupMembership) { + return null + } + + return parsePermissionGroupConfig(groupMembership.config) +} + +export async function validateModelProvider( + userId: string | undefined, + model: string +): Promise { + if (!userId) { + return + } + + const config = await getUserPermissionConfig(userId) + + if (!config || config.allowedModelProviders === null) { + return + } + + const providerId = getProviderFromModel(model) + + if (!config.allowedModelProviders.includes(providerId)) { + logger.warn('Model provider blocked by permission group', { userId, model, providerId }) + throw new ProviderNotAllowedError(providerId, model) + } +} + +export async function validateBlockType( + userId: string | undefined, + blockType: string +): Promise { + if (!userId) { + return + } + + const config = await getUserPermissionConfig(userId) + + if (!config || config.allowedIntegrations === null) { + return + } + + if (!config.allowedIntegrations.includes(blockType)) { + logger.warn('Integration blocked by permission group', { userId, blockType }) + throw new IntegrationNotAllowedError(blockType) + } +} diff --git a/apps/sim/hooks/queries/permission-groups.ts b/apps/sim/hooks/queries/permission-groups.ts new file mode 100644 index 0000000000..de62725579 --- /dev/null +++ b/apps/sim/hooks/queries/permission-groups.ts @@ -0,0 +1,282 @@ +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import type { PermissionGroupConfig } from '@/lib/permission-groups/types' +import { fetchJson } from '@/hooks/selectors/helpers' + +export interface PermissionGroup { + id: string + name: string + description: string | null + config: PermissionGroupConfig + createdBy: string + createdAt: string + updatedAt: string + creatorName: string | null + creatorEmail: string | null + memberCount: number +} + +export interface PermissionGroupMember { + id: string + userId: string + assignedAt: string + userName: string | null + userEmail: string | null + userImage: string | null +} + +export interface UserPermissionConfig { + permissionGroupId: string | null + groupName: string | null + config: PermissionGroupConfig | null +} + +export const permissionGroupKeys = { + all: ['permissionGroups'] as const, + list: (organizationId?: string) => + ['permissionGroups', 'list', organizationId ?? 'none'] as const, + detail: (id?: string) => ['permissionGroups', 'detail', id ?? 'none'] as const, + members: (id?: string) => ['permissionGroups', 'members', id ?? 'none'] as const, + userConfig: (organizationId?: string) => + ['permissionGroups', 'userConfig', organizationId ?? 'none'] as const, +} + +interface PermissionGroupsResponse { + permissionGroups?: PermissionGroup[] +} + +export function usePermissionGroups(organizationId?: string, enabled = true) { + return useQuery({ + queryKey: permissionGroupKeys.list(organizationId), + queryFn: async () => { + const data = await fetchJson('/api/permission-groups', { + searchParams: { organizationId: organizationId ?? '' }, + }) + return data.permissionGroups ?? [] + }, + enabled: Boolean(organizationId) && enabled, + staleTime: 60 * 1000, + }) +} + +interface PermissionGroupDetailResponse { + permissionGroup?: PermissionGroup +} + +export function usePermissionGroup(id?: string, enabled = true) { + return useQuery({ + queryKey: permissionGroupKeys.detail(id), + queryFn: async () => { + const data = await fetchJson(`/api/permission-groups/${id}`) + return data.permissionGroup ?? null + }, + enabled: Boolean(id) && enabled, + staleTime: 60 * 1000, + }) +} + +interface MembersResponse { + members?: PermissionGroupMember[] +} + +export function usePermissionGroupMembers(permissionGroupId?: string) { + return useQuery({ + queryKey: permissionGroupKeys.members(permissionGroupId), + queryFn: async () => { + const data = await fetchJson( + `/api/permission-groups/${permissionGroupId}/members` + ) + return data.members ?? [] + }, + enabled: Boolean(permissionGroupId), + staleTime: 30 * 1000, + }) +} + +export function useUserPermissionConfig(organizationId?: string) { + return useQuery({ + queryKey: permissionGroupKeys.userConfig(organizationId), + queryFn: async () => { + const data = await fetchJson('/api/permission-groups/user', { + searchParams: { organizationId: organizationId ?? '' }, + }) + return data + }, + enabled: Boolean(organizationId), + staleTime: 60 * 1000, + }) +} + +export interface CreatePermissionGroupData { + organizationId: string + name: string + description?: string + config?: Partial +} + +export function useCreatePermissionGroup() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (data: CreatePermissionGroupData) => { + const response = await fetch('/api/permission-groups', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to create permission group') + } + return response.json() + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.list(variables.organizationId), + }) + }, + }) +} + +export interface UpdatePermissionGroupData { + id: string + organizationId: string + name?: string + description?: string | null + config?: Partial +} + +export function useUpdatePermissionGroup() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ id, ...data }: UpdatePermissionGroupData) => { + const response = await fetch(`/api/permission-groups/${id}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to update permission group') + } + return response.json() + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.list(variables.organizationId), + }) + queryClient.invalidateQueries({ queryKey: permissionGroupKeys.detail(variables.id) }) + queryClient.invalidateQueries({ queryKey: ['permissionGroups', 'userConfig'] }) + }, + }) +} + +export interface DeletePermissionGroupParams { + permissionGroupId: string + organizationId: string +} + +export function useDeletePermissionGroup() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ permissionGroupId }: DeletePermissionGroupParams) => { + const response = await fetch(`/api/permission-groups/${permissionGroupId}`, { + method: 'DELETE', + }) + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to delete permission group') + } + return response.json() + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.list(variables.organizationId), + }) + queryClient.invalidateQueries({ queryKey: ['permissionGroups', 'userConfig'] }) + }, + }) +} + +export function useAddPermissionGroupMember() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (data: { permissionGroupId: string; userId: string }) => { + const response = await fetch(`/api/permission-groups/${data.permissionGroupId}/members`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ userId: data.userId }), + }) + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to add member') + } + return response.json() + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.members(variables.permissionGroupId), + }) + queryClient.invalidateQueries({ queryKey: permissionGroupKeys.all }) + }, + }) +} + +export function useRemovePermissionGroupMember() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async (data: { permissionGroupId: string; memberId: string }) => { + const response = await fetch( + `/api/permission-groups/${data.permissionGroupId}/members?memberId=${data.memberId}`, + { method: 'DELETE' } + ) + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to remove member') + } + return response.json() + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.members(variables.permissionGroupId), + }) + queryClient.invalidateQueries({ queryKey: permissionGroupKeys.all }) + queryClient.invalidateQueries({ queryKey: ['permissionGroups', 'userConfig'] }) + }, + }) +} + +export interface BulkAddMembersData { + permissionGroupId: string + userIds?: string[] + addAllOrgMembers?: boolean +} + +export function useBulkAddPermissionGroupMembers() { + const queryClient = useQueryClient() + + return useMutation({ + mutationFn: async ({ permissionGroupId, ...data }: BulkAddMembersData) => { + const response = await fetch(`/api/permission-groups/${permissionGroupId}/members/bulk`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(data), + }) + if (!response.ok) { + const result = await response.json() + throw new Error(result.error || 'Failed to add members') + } + return response.json() as Promise<{ added: number; moved: number }> + }, + onSuccess: (_data, variables) => { + queryClient.invalidateQueries({ + queryKey: permissionGroupKeys.members(variables.permissionGroupId), + }) + queryClient.invalidateQueries({ queryKey: permissionGroupKeys.all }) + queryClient.invalidateQueries({ queryKey: ['permissionGroups', 'userConfig'] }) + }, + }) +} diff --git a/apps/sim/hooks/use-permission-config.ts b/apps/sim/hooks/use-permission-config.ts new file mode 100644 index 0000000000..584930f060 --- /dev/null +++ b/apps/sim/hooks/use-permission-config.ts @@ -0,0 +1,71 @@ +import { useMemo } from 'react' +import { + DEFAULT_PERMISSION_GROUP_CONFIG, + type PermissionGroupConfig, +} from '@/lib/permission-groups/types' +import { useOrganizations } from '@/hooks/queries/organization' +import { useUserPermissionConfig } from '@/hooks/queries/permission-groups' + +export interface PermissionConfigResult { + config: PermissionGroupConfig + isLoading: boolean + isInPermissionGroup: boolean + filterBlocks: (blocks: T[]) => T[] + filterProviders: (providerIds: string[]) => string[] + isBlockAllowed: (blockType: string) => boolean + isProviderAllowed: (providerId: string) => boolean +} + +export function usePermissionConfig(): PermissionConfigResult { + const { data: organizationsData } = useOrganizations() + const activeOrganization = organizationsData?.activeOrganization + + const { data: permissionData, isLoading } = useUserPermissionConfig(activeOrganization?.id) + + const config = useMemo(() => { + if (!permissionData?.config) { + return DEFAULT_PERMISSION_GROUP_CONFIG + } + return permissionData.config + }, [permissionData]) + + const isInPermissionGroup = !!permissionData?.permissionGroupId + + const isBlockAllowed = useMemo(() => { + return (blockType: string) => { + if (config.allowedIntegrations === null) return true + return config.allowedIntegrations.includes(blockType) + } + }, [config.allowedIntegrations]) + + const isProviderAllowed = useMemo(() => { + return (providerId: string) => { + if (config.allowedModelProviders === null) return true + return config.allowedModelProviders.includes(providerId) + } + }, [config.allowedModelProviders]) + + const filterBlocks = useMemo(() => { + return (blocks: T[]): T[] => { + if (config.allowedIntegrations === null) return blocks + return blocks.filter((block) => config.allowedIntegrations!.includes(block.type)) + } + }, [config.allowedIntegrations]) + + const filterProviders = useMemo(() => { + return (providerIds: string[]): string[] => { + if (config.allowedModelProviders === null) return providerIds + return providerIds.filter((id) => config.allowedModelProviders!.includes(id)) + } + }, [config.allowedModelProviders]) + + return { + config, + isLoading, + isInPermissionGroup, + filterBlocks, + filterProviders, + isBlockAllowed, + isProviderAllowed, + } +} diff --git a/apps/sim/lib/copilot/process-contents.ts b/apps/sim/lib/copilot/process-contents.ts index 3a18495b90..ccb690964e 100644 --- a/apps/sim/lib/copilot/process-contents.ts +++ b/apps/sim/lib/copilot/process-contents.ts @@ -5,6 +5,7 @@ import { and, eq, isNull } from 'drizzle-orm' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer' import { escapeRegExp } from '@/executor/constants' +import { getUserPermissionConfig } from '@/executor/utils/permission-check' import type { ChatContext } from '@/stores/panel/copilot/types' export type AgentContextType = @@ -104,7 +105,11 @@ export async function processContextsServer( ) } if (ctx.kind === 'blocks' && (ctx as any).blockId) { - return await processBlockMetadata((ctx as any).blockId, ctx.label ? `@${ctx.label}` : '@') + return await processBlockMetadata( + (ctx as any).blockId, + ctx.label ? `@${ctx.label}` : '@', + userId + ) } if (ctx.kind === 'templates' && (ctx as any).templateId) { return await processTemplateFromDb( @@ -355,8 +360,21 @@ async function processKnowledgeFromDb( } } -async function processBlockMetadata(blockId: string, tag: string): Promise { +async function processBlockMetadata( + blockId: string, + tag: string, + userId?: string +): Promise { try { + if (userId) { + const permissionConfig = await getUserPermissionConfig(userId) + const allowedIntegrations = permissionConfig?.allowedIntegrations + if (allowedIntegrations !== null && !allowedIntegrations?.includes(blockId)) { + logger.debug('Block not allowed by permission group', { blockId, userId }) + return null + } + } + // Reuse registry to match get_blocks_metadata tool result const { registry: blockRegistry } = await import('@/blocks/registry') const { tools: toolsRegistry } = await import('@/tools/registry') diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts b/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts index 72ce42c6a7..06e8419259 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-block-config.ts @@ -7,6 +7,7 @@ import { } from '@/lib/copilot/tools/shared/schemas' import { registry as blockRegistry } from '@/blocks/registry' import type { SubBlockConfig } from '@/blocks/types' +import { getUserPermissionConfig } from '@/executor/utils/permission-check' import { PROVIDER_DEFINITIONS } from '@/providers/models' import { tools as toolsRegistry } from '@/tools/registry' @@ -298,13 +299,20 @@ export const getBlockConfigServerTool: BaseServerTool< GetBlockConfigResultType > = { name: 'get_block_config', - async execute({ - blockType, - operation, - }: GetBlockConfigInputType): Promise { + async execute( + { blockType, operation }: GetBlockConfigInputType, + context?: { userId: string } + ): Promise { const logger = createLogger('GetBlockConfigServerTool') logger.debug('Executing get_block_config', { blockType, operation }) + const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + const allowedIntegrations = permissionConfig?.allowedIntegrations + + if (allowedIntegrations !== null && !allowedIntegrations?.includes(blockType)) { + throw new Error(`Block "${blockType}" is not available`) + } + const blockConfig = blockRegistry[blockType] if (!blockConfig) { throw new Error(`Block not found: ${blockType}`) diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts b/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts index 9ac56dc0eb..9898b11c21 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-block-options.ts @@ -6,6 +6,7 @@ import { type GetBlockOptionsResultType, } from '@/lib/copilot/tools/shared/schemas' import { registry as blockRegistry } from '@/blocks/registry' +import { getUserPermissionConfig } from '@/executor/utils/permission-check' import { tools as toolsRegistry } from '@/tools/registry' export const getBlockOptionsServerTool: BaseServerTool< @@ -13,10 +14,20 @@ export const getBlockOptionsServerTool: BaseServerTool< GetBlockOptionsResultType > = { name: 'get_block_options', - async execute({ blockId }: GetBlockOptionsInputType): Promise { + async execute( + { blockId }: GetBlockOptionsInputType, + context?: { userId: string } + ): Promise { const logger = createLogger('GetBlockOptionsServerTool') logger.debug('Executing get_block_options', { blockId }) + const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + const allowedIntegrations = permissionConfig?.allowedIntegrations + + if (allowedIntegrations !== null && !allowedIntegrations?.includes(blockId)) { + throw new Error(`Block "${blockId}" is not available`) + } + const blockConfig = blockRegistry[blockId] if (!blockConfig) { throw new Error(`Block not found: ${blockId}`) diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts index 90b5381297..48851448dd 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-and-tools.ts @@ -6,16 +6,20 @@ import { } from '@/lib/copilot/tools/shared/schemas' import { registry as blockRegistry } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' +import { getUserPermissionConfig } from '@/executor/utils/permission-check' export const getBlocksAndToolsServerTool: BaseServerTool< ReturnType, ReturnType > = { name: 'get_blocks_and_tools', - async execute() { + async execute(_args: unknown, context?: { userId: string }) { const logger = createLogger('GetBlocksAndToolsServerTool') logger.debug('Executing get_blocks_and_tools') + const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + const allowedIntegrations = permissionConfig?.allowedIntegrations + type BlockListItem = { type: string name: string @@ -25,7 +29,11 @@ export const getBlocksAndToolsServerTool: BaseServerTool< const blocks: BlockListItem[] = [] Object.entries(blockRegistry) - .filter(([, blockConfig]: [string, BlockConfig]) => !blockConfig.hideFromToolbar) + .filter(([blockType, blockConfig]: [string, BlockConfig]) => { + if (blockConfig.hideFromToolbar) return false + if (allowedIntegrations !== null && !allowedIntegrations?.includes(blockType)) return false + return true + }) .forEach(([blockType, blockConfig]: [string, BlockConfig]) => { blocks.push({ type: blockType, diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts index f27adb8df7..86f1bbc172 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-blocks-metadata-tool.ts @@ -9,6 +9,7 @@ import { import { registry as blockRegistry } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' import { AuthMode } from '@/blocks/types' +import { getUserPermissionConfig } from '@/executor/utils/permission-check' import { PROVIDER_DEFINITIONS } from '@/providers/models' import { tools as toolsRegistry } from '@/tools/registry' import { getTrigger, isTriggerValid } from '@/triggers' @@ -105,16 +106,23 @@ export const getBlocksMetadataServerTool: BaseServerTool< ReturnType > = { name: 'get_blocks_metadata', - async execute({ - blockIds, - }: ReturnType): Promise< - ReturnType - > { + async execute( + { blockIds }: ReturnType, + context?: { userId: string } + ): Promise> { const logger = createLogger('GetBlocksMetadataServerTool') logger.debug('Executing get_blocks_metadata', { count: blockIds?.length }) + const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + const allowedIntegrations = permissionConfig?.allowedIntegrations + const result: Record = {} for (const blockId of blockIds || []) { + if (allowedIntegrations !== null && !allowedIntegrations?.includes(blockId)) { + logger.debug('Block not allowed by permission group', { blockId }) + continue + } + let metadata: any if (SPECIAL_BLOCKS_METADATA[blockId]) { diff --git a/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts index 2f3ee142b0..3e6f84ed25 100644 --- a/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts +++ b/apps/sim/lib/copilot/tools/server/blocks/get-trigger-blocks.ts @@ -3,6 +3,7 @@ import { z } from 'zod' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { registry as blockRegistry } from '@/blocks/registry' import type { BlockConfig } from '@/blocks/types' +import { getUserPermissionConfig } from '@/executor/utils/permission-check' export const GetTriggerBlocksInput = z.object({}) export const GetTriggerBlocksResult = z.object({ @@ -14,14 +15,18 @@ export const getTriggerBlocksServerTool: BaseServerTool< ReturnType > = { name: 'get_trigger_blocks', - async execute() { + async execute(_args: unknown, context?: { userId: string }) { const logger = createLogger('GetTriggerBlocksServerTool') logger.debug('Executing get_trigger_blocks') + const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + const allowedIntegrations = permissionConfig?.allowedIntegrations + const triggerBlockIds: string[] = [] Object.entries(blockRegistry).forEach(([blockType, blockConfig]: [string, BlockConfig]) => { if (blockConfig.hideFromToolbar) return + if (allowedIntegrations !== null && !allowedIntegrations?.includes(blockType)) return if (blockConfig.category === 'triggers') { triggerBlockIds.push(blockType) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 86fe3669af..4a7d69ee72 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -5,6 +5,7 @@ import { createLogger } from '@sim/logger' import { eq } from 'drizzle-orm' import type { BaseServerTool } from '@/lib/copilot/tools/server/base-tool' import { validateSelectorIds } from '@/lib/copilot/validation/selector-validator' +import type { PermissionGroupConfig } from '@/lib/permission-groups/types' import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs' import { extractAndPersistCustomTools } from '@/lib/workflows/persistence/custom-tools-persistence' import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils' @@ -13,6 +14,7 @@ import { validateWorkflowState } from '@/lib/workflows/sanitization/validation' import { getAllBlocks, getBlock } from '@/blocks/registry' import type { SubBlockConfig } from '@/blocks/types' import { EDGE, normalizeName } from '@/executor/constants' +import { getUserPermissionConfig } from '@/executor/utils/permission-check' import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils' import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants' @@ -49,6 +51,7 @@ interface ValidationError { type SkippedItemType = | 'block_not_found' | 'invalid_block_type' + | 'block_not_allowed' | 'invalid_edge_target' | 'invalid_edge_source' | 'invalid_source_handle' @@ -1093,12 +1096,26 @@ interface ApplyOperationsResult { skippedItems: SkippedItem[] } +/** + * Checks if a block type is allowed by the permission group config + */ +function isBlockTypeAllowed( + blockType: string, + permissionConfig: PermissionGroupConfig | null +): boolean { + if (!permissionConfig || permissionConfig.allowedIntegrations === null) { + return true + } + return permissionConfig.allowedIntegrations.includes(blockType) +} + /** * Apply operations directly to the workflow JSON state */ function applyOperationsToWorkflowState( workflowState: any, - operations: EditWorkflowOperation[] + operations: EditWorkflowOperation[], + permissionConfig: PermissionGroupConfig | null = null ): ApplyOperationsResult { // Deep clone the workflow state to avoid mutations const modifiedState = JSON.parse(JSON.stringify(workflowState)) @@ -1401,6 +1418,14 @@ function applyOperationsToWorkflowState( reason: `Invalid block type "${params.type}" - type change skipped`, details: { requestedType: params.type }, }) + } else if (!isContainerType && !isBlockTypeAllowed(params.type, permissionConfig)) { + logSkippedItem(skippedItems, { + type: 'block_not_allowed', + operationType: 'edit', + blockId: block_id, + reason: `Block type "${params.type}" is not allowed by permission group - type change skipped`, + details: { requestedType: params.type }, + }) } else { block.type = params.type } @@ -1680,6 +1705,18 @@ function applyOperationsToWorkflowState( break } + // Check if block type is allowed by permission group + if (!isContainerType && !isBlockTypeAllowed(params.type, permissionConfig)) { + logSkippedItem(skippedItems, { + type: 'block_not_allowed', + operationType: 'add', + blockId: block_id, + reason: `Block type "${params.type}" is not allowed by permission group - block not added`, + details: { requestedType: params.type }, + }) + break + } + // Create new block with proper structure const newBlock = createBlockFromParams(block_id, params, undefined, validationErrors) @@ -1920,6 +1957,18 @@ function applyOperationsToWorkflowState( break } + // Check if block type is allowed by permission group + if (!isContainerType && !isBlockTypeAllowed(params.type, permissionConfig)) { + logSkippedItem(skippedItems, { + type: 'block_not_allowed', + operationType: 'insert_into_subflow', + blockId: block_id, + reason: `Block type "${params.type}" is not allowed by permission group - block not inserted`, + details: { requestedType: params.type, subflowId }, + }) + break + } + // Create new block as child of subflow const newBlock = createBlockFromParams(block_id, params, subflowId, validationErrors) modifiedState.blocks[block_id] = newBlock @@ -2223,12 +2272,15 @@ export const editWorkflowServerTool: BaseServerTool = { workflowState = fromDb.workflowState } + // Get permission config for the user + const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + // Apply operations directly to the workflow state const { state: modifiedWorkflowState, validationErrors, skippedItems, - } = applyOperationsToWorkflowState(workflowState, operations) + } = applyOperationsToWorkflowState(workflowState, operations, permissionConfig) // Get workspaceId for selector validation let workspaceId: string | undefined diff --git a/apps/sim/lib/permission-groups/types.ts b/apps/sim/lib/permission-groups/types.ts new file mode 100644 index 0000000000..c3a6adfddb --- /dev/null +++ b/apps/sim/lib/permission-groups/types.ts @@ -0,0 +1,25 @@ +export interface PermissionGroupConfig { + allowedIntegrations: string[] | null + allowedModelProviders: string[] | null + hideTraceSpans: boolean +} + +export const DEFAULT_PERMISSION_GROUP_CONFIG: PermissionGroupConfig = { + allowedIntegrations: null, + allowedModelProviders: null, + hideTraceSpans: false, +} + +export function parsePermissionGroupConfig(config: unknown): PermissionGroupConfig { + if (!config || typeof config !== 'object') { + return DEFAULT_PERMISSION_GROUP_CONFIG + } + + const c = config as Record + + return { + allowedIntegrations: Array.isArray(c.allowedIntegrations) ? c.allowedIntegrations : null, + allowedModelProviders: Array.isArray(c.allowedModelProviders) ? c.allowedModelProviders : null, + hideTraceSpans: typeof c.hideTraceSpans === 'boolean' ? c.hideTraceSpans : false, + } +} diff --git a/packages/db/schema.ts b/packages/db/schema.ts index c03d4e65b3..d3a4a6b6fe 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1877,3 +1877,52 @@ export const credentialSetInvitation = pgTable( expiresAtIdx: index('credential_set_invitation_expires_at_idx').on(table.expiresAt), }) ) + +export const permissionGroup = pgTable( + 'permission_group', + { + id: text('id').primaryKey(), + organizationId: text('organization_id') + .notNull() + .references(() => organization.id, { onDelete: 'cascade' }), + name: text('name').notNull(), + description: text('description'), + config: jsonb('config').notNull().default('{}'), + createdBy: text('created_by') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + createdAt: timestamp('created_at').notNull().defaultNow(), + updatedAt: timestamp('updated_at').notNull().defaultNow(), + }, + (table) => ({ + organizationIdIdx: index('permission_group_organization_id_idx').on(table.organizationId), + createdByIdx: index('permission_group_created_by_idx').on(table.createdBy), + orgNameUnique: uniqueIndex('permission_group_org_name_unique').on( + table.organizationId, + table.name + ), + }) +) + +export const permissionGroupMember = pgTable( + 'permission_group_member', + { + id: text('id').primaryKey(), + permissionGroupId: text('permission_group_id') + .notNull() + .references(() => permissionGroup.id, { onDelete: 'cascade' }), + userId: text('user_id') + .notNull() + .references(() => user.id, { onDelete: 'cascade' }), + assignedBy: text('assigned_by').references(() => user.id, { onDelete: 'set null' }), + assignedAt: timestamp('assigned_at').notNull().defaultNow(), + }, + (table) => ({ + permissionGroupIdIdx: index('permission_group_member_group_id_idx').on(table.permissionGroupId), + userIdIdx: index('permission_group_member_user_id_idx').on(table.userId), + uniqueMembership: uniqueIndex('permission_group_member_unique').on( + table.permissionGroupId, + table.userId + ), + }) +) From 0cb6de4229e7d3c9643c9bc53748d2643c5dd03d Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 14:57:08 -0800 Subject: [PATCH 02/14] feat: enterprise gating for BYOK, SSO, credential sets with org admin/owner checks --- .../[workspaceId]/knowledge/page.tsx | 34 +- .../[workspaceId]/templates/page.tsx | 7 + .../components/tool-input/tool-input.tsx | 56 +-- .../panel/components/toolbar/toolbar.tsx | 5 +- .../w/[workflowId]/components/panel/panel.tsx | 54 +-- .../access-control/access-control.tsx | 374 ++++++++++++++++-- .../components/integrations/integrations.tsx | 12 +- .../settings-modal/settings-modal.tsx | 23 ++ .../w/components/sidebar/sidebar.tsx | 71 ++-- apps/sim/lib/permission-groups/types.ts | 26 ++ 10 files changed, 550 insertions(+), 112 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx b/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx index d458d683c7..a5c1eadeb4 100644 --- a/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/knowledge/page.tsx @@ -1 +1,33 @@ -export { Knowledge as default } from './knowledge' +import { redirect } from 'next/navigation' +import { getSession } from '@/lib/auth' +import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' +import { getUserPermissionConfig } from '@/executor/utils/permission-check' +import { Knowledge } from './knowledge' + +interface KnowledgePageProps { + params: Promise<{ + workspaceId: string + }> +} + +export default async function KnowledgePage({ params }: KnowledgePageProps) { + const { workspaceId } = await params + const session = await getSession() + + if (!session?.user?.id) { + redirect('/') + } + + const hasPermission = await verifyWorkspaceMembership(session.user.id, workspaceId) + if (!hasPermission) { + redirect('/') + } + + // Check permission group restrictions + const permissionConfig = await getUserPermissionConfig(session.user.id) + if (permissionConfig?.hideKnowledgeBaseTab) { + redirect(`/workspace/${workspaceId}`) + } + + return +} diff --git a/apps/sim/app/workspace/[workspaceId]/templates/page.tsx b/apps/sim/app/workspace/[workspaceId]/templates/page.tsx index 0e49d77b5e..9955c24331 100644 --- a/apps/sim/app/workspace/[workspaceId]/templates/page.tsx +++ b/apps/sim/app/workspace/[workspaceId]/templates/page.tsx @@ -6,6 +6,7 @@ import { getSession } from '@/lib/auth' import { verifyWorkspaceMembership } from '@/app/api/workflows/utils' import type { Template as WorkspaceTemplate } from '@/app/workspace/[workspaceId]/templates/templates' import Templates from '@/app/workspace/[workspaceId]/templates/templates' +import { getUserPermissionConfig } from '@/executor/utils/permission-check' interface TemplatesPageProps { params: Promise<{ @@ -32,6 +33,12 @@ export default async function TemplatesPage({ params }: TemplatesPageProps) { redirect('/') } + // Check permission group restrictions + const permissionConfig = await getUserPermissionConfig(session.user.id) + if (permissionConfig?.hideTemplates) { + redirect(`/workspace/${workspaceId}`) + } + // Determine effective super user (DB flag AND UI mode enabled) const currentUser = await db .select({ isSuperUser: user.isSuperUser }) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 03b37d955d..8a380eace9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -1004,7 +1004,7 @@ export function ToolInput({ const provider = model ? getProviderFromModel(model) : '' const supportsToolControl = provider ? supportsToolUsageControl(provider) : false - const { filterBlocks } = usePermissionConfig() + const { filterBlocks, config: permissionConfig } = usePermissionConfig() const toolBlocks = useMemo(() => { const allToolBlocks = getAllBlocks().filter( @@ -1608,33 +1608,37 @@ export function ToolInput({ const groups: ComboboxOptionGroup[] = [] // Actions group (no section header) - groups.push({ - items: [ - { - label: 'Create Tool', - value: 'action-create-tool', - icon: WrenchIcon, - onSelect: () => { - setCustomToolModalOpen(true) - setOpen(false) - }, - disabled: isPreview, + const actionItems: ComboboxOption[] = [] + if (!permissionConfig.disableCustomTools) { + actionItems.push({ + label: 'Create Tool', + value: 'action-create-tool', + icon: WrenchIcon, + onSelect: () => { + setCustomToolModalOpen(true) + setOpen(false) }, - { - label: 'Add MCP Server', - value: 'action-add-mcp', - icon: McpIcon, - onSelect: () => { - setOpen(false) - window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'mcp' } })) - }, - disabled: isPreview, + disabled: isPreview, + }) + } + if (!permissionConfig.disableMcpTools) { + actionItems.push({ + label: 'Add MCP Server', + value: 'action-add-mcp', + icon: McpIcon, + onSelect: () => { + setOpen(false) + window.dispatchEvent(new CustomEvent('open-settings', { detail: { tab: 'mcp' } })) }, - ], - }) + disabled: isPreview, + }) + } + if (actionItems.length > 0) { + groups.push({ items: actionItems }) + } // Custom Tools section - if (customTools.length > 0) { + if (!permissionConfig.disableCustomTools && customTools.length > 0) { groups.push({ section: 'Custom Tools', items: customTools.map((customTool) => ({ @@ -1659,7 +1663,7 @@ export function ToolInput({ } // MCP Tools section - if (availableMcpTools.length > 0) { + if (!permissionConfig.disableMcpTools && availableMcpTools.length > 0) { groups.push({ section: 'MCP Tools', items: availableMcpTools.map((mcpTool) => { @@ -1736,6 +1740,8 @@ export function ToolInput({ setStoreValue, handleMcpToolSelect, handleSelectTool, + permissionConfig.disableCustomTools, + permissionConfig.disableMcpTools, ]) const toolRequiresOAuth = (toolId: string): boolean => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx index 4ebc1a73fa..8c7bb3d038 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/toolbar/toolbar.tsx @@ -197,11 +197,12 @@ export const Toolbar = forwardRef(function Toolbar( const { filterBlocks } = usePermissionConfig() // Get static data (computed once and cached) - const triggers = getTriggers() + const allTriggers = getTriggers() const allBlocks = getBlocks() - // Apply permission-based filtering to blocks + // Apply permission-based filtering to blocks and triggers const blocks = useMemo(() => filterBlocks(allBlocks), [filterBlocks, allBlocks]) + const triggers = useMemo(() => filterBlocks(allTriggers), [filterBlocks, allTriggers]) // Determine if triggers are at minimum height (blocks are fully expanded) const isTriggersAtMinimum = toolbarTriggersHeight <= TRIGGERS_MIN_THRESHOLD diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx index 7dda151af0..c6fb11539d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/panel.tsx @@ -40,6 +40,7 @@ import { Variables } from '@/app/workspace/[workspaceId]/w/[workflowId]/componen import { useAutoLayout } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-auto-layout' import { useWorkflowExecution } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-workflow-execution' import { useDeleteWorkflow, useImportWorkflow } from '@/app/workspace/[workspaceId]/w/hooks' +import { usePermissionConfig } from '@/hooks/use-permission-config' import { useChatStore } from '@/stores/chat/store' import { usePanelStore } from '@/stores/panel/store' import type { PanelTab } from '@/stores/panel/types' @@ -92,6 +93,7 @@ export function Panel() { // Hooks const userPermissions = useUserPermissionsContext() + const { config: permissionConfig } = usePermissionConfig() const { isImporting, handleFileChange } = useImportWorkflow({ workspaceId }) const { workflows, activeWorkflowId, duplicateWorkflow, hydration } = useWorkflowRegistry() const isRegistryLoading = @@ -438,18 +440,20 @@ export function Panel() { {/* Tabs */}
- + {!permissionConfig.hideCopilot && ( + + )} +

Select which model providers are available in agent dropdowns. All are allowed by default.

+
+ + setProviderSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-[13px] leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
- {allProviderIds.map((providerId) => ( + {filteredProviders.map((providerId) => ( +

Select which integrations are visible in the toolbar. All are visible by default.

+
+ + setIntegrationSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-[13px] leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
- {allBlocks.map((block) => ( + {filteredBlocks.map((block) => (
+ +
+
+ + +
+

+ Checked features are visible. Uncheck to hide. +

+
+ {/* Sidebar */} +
+ + Sidebar + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, hideKnowledgeBaseTab: checked !== true } : prev + ) + } + /> + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, hideTemplates: checked !== true } : prev + ) + } + /> + +
+
+
+ + {/* Workflow Panel */} +
+ + Workflow Panel + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, hideCopilot: checked !== true } : prev + ) + } + /> + +
+
+
+ + {/* Settings Tabs */} +
+ + Settings Tabs + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, hideApiKeysTab: checked !== true } : prev + ) + } + /> + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, hideEnvironmentTab: checked !== true } : prev + ) + } + /> + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, hideFilesTab: checked !== true } : prev + ) + } + /> + +
+
+
+ + {/* Tools */} +
+ + Tools + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, disableMcpTools: checked !== true } : prev + ) + } + /> + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, disableCustomTools: checked !== true } : prev + ) + } + /> + +
+
+
+ + {/* Logs */} +
+ + Logs + +
+
+ + setEditingConfig((prev) => + prev ? { ...prev, hideTraceSpans: checked !== true } : prev + ) + } + /> + +
+
+
+
+
-

- Select which integrations are visible in the toolbar. All are visible by - default. + Select which blocks are visible in the toolbar. All are visible by default.

setIntegrationSearchTerm(e.target.value)} className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-[13px] leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' @@ -633,7 +633,7 @@ export function AccessControl() {
{/* Sidebar */}
- + Sidebar
@@ -649,7 +649,7 @@ export function AccessControl() { /> @@ -666,7 +666,7 @@ export function AccessControl() { /> @@ -676,7 +676,7 @@ export function AccessControl() { {/* Workflow Panel */}
- + Workflow Panel
@@ -692,7 +692,7 @@ export function AccessControl() { /> @@ -702,7 +702,7 @@ export function AccessControl() { {/* Settings Tabs */}
- + Settings Tabs
@@ -718,7 +718,7 @@ export function AccessControl() { /> @@ -735,7 +735,7 @@ export function AccessControl() { /> @@ -752,7 +752,7 @@ export function AccessControl() { /> @@ -762,7 +762,7 @@ export function AccessControl() { {/* Tools */}
- + Tools
@@ -778,7 +778,7 @@ export function AccessControl() { /> @@ -795,7 +795,7 @@ export function AccessControl() { /> @@ -805,7 +805,7 @@ export function AccessControl() { {/* Logs */}
- + Logs
@@ -821,7 +821,7 @@ export function AccessControl() { /> diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 206684b169..6640546f4a 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -25,7 +25,12 @@ import type { BlockHandler, ExecutionContext, StreamingExecution } from '@/execu import { collectBlockData } from '@/executor/utils/block-data' import { buildAPIUrl, buildAuthHeaders, extractAPIErrorMessage } from '@/executor/utils/http' import { stringifyJSON } from '@/executor/utils/json' -import { validateBlockType, validateModelProvider } from '@/executor/utils/permission-check' +import { + validateBlockType, + validateCustomToolsAllowed, + validateMcpToolsAllowed, + validateModelProvider, +} from '@/executor/utils/permission-check' import { executeProviderRequest } from '@/providers' import { getProviderFromModel, transformBlockTool } from '@/providers/utils' import type { SerializedBlock } from '@/serializer/types' @@ -51,6 +56,9 @@ export class AgentBlockHandler implements BlockHandler { const filteredTools = await this.filterUnavailableMcpTools(ctx, inputs.tools || []) const filteredInputs = { ...inputs, tools: filteredTools } + // Validate tool permissions before processing + await this.validateToolPermissions(ctx, filteredInputs.tools || []) + const responseFormat = this.parseResponseFormat(filteredInputs.responseFormat) const model = filteredInputs.model || AGENT.DEFAULT_MODEL @@ -147,6 +155,21 @@ export class AgentBlockHandler implements BlockHandler { return undefined } + private async validateToolPermissions(ctx: ExecutionContext, tools: ToolInput[]): Promise { + if (!Array.isArray(tools) || tools.length === 0) return + + const hasMcpTools = tools.some((t) => t.type === 'mcp') + const hasCustomTools = tools.some((t) => t.type === 'custom-tool') + + if (hasMcpTools) { + await validateMcpToolsAllowed(ctx.userId) + } + + if (hasCustomTools) { + await validateCustomToolsAllowed(ctx.userId) + } + } + private async filterUnavailableMcpTools( ctx: ExecutionContext, tools: ToolInput[] diff --git a/apps/sim/executor/utils/permission-check.ts b/apps/sim/executor/utils/permission-check.ts index 50eef90882..14205c1e26 100644 --- a/apps/sim/executor/utils/permission-check.ts +++ b/apps/sim/executor/utils/permission-check.ts @@ -26,6 +26,20 @@ export class IntegrationNotAllowedError extends Error { } } +export class McpToolsNotAllowedError extends Error { + constructor() { + super('MCP tools are not allowed based on your permission group settings') + this.name = 'McpToolsNotAllowedError' + } +} + +export class CustomToolsNotAllowedError extends Error { + constructor() { + super('Custom tools are not allowed based on your permission group settings') + this.name = 'CustomToolsNotAllowedError' + } +} + export async function getUserPermissionConfig( userId: string ): Promise { @@ -99,3 +113,37 @@ export async function validateBlockType( throw new IntegrationNotAllowedError(blockType) } } + +export async function validateMcpToolsAllowed(userId: string | undefined): Promise { + if (!userId) { + return + } + + const config = await getUserPermissionConfig(userId) + + if (!config) { + return + } + + if (config.disableMcpTools) { + logger.warn('MCP tools blocked by permission group', { userId }) + throw new McpToolsNotAllowedError() + } +} + +export async function validateCustomToolsAllowed(userId: string | undefined): Promise { + if (!userId) { + return + } + + const config = await getUserPermissionConfig(userId) + + if (!config) { + return + } + + if (config.disableCustomTools) { + logger.warn('Custom tools blocked by permission group', { userId }) + throw new CustomToolsNotAllowedError() + } +} From adf088a10796baa727fab96b4ec7670ca80dfec8 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 15:51:02 -0800 Subject: [PATCH 04/14] add admin routes to cleanup permission group data --- .../app/api/v1/admin/access-control/route.ts | 169 ++++++++++++++++++ apps/sim/app/api/v1/admin/index.ts | 4 + 2 files changed, 173 insertions(+) create mode 100644 apps/sim/app/api/v1/admin/access-control/route.ts diff --git a/apps/sim/app/api/v1/admin/access-control/route.ts b/apps/sim/app/api/v1/admin/access-control/route.ts new file mode 100644 index 0000000000..7da37edc8e --- /dev/null +++ b/apps/sim/app/api/v1/admin/access-control/route.ts @@ -0,0 +1,169 @@ +/** + * Admin Access Control (Permission Groups) API + * + * GET /api/v1/admin/access-control + * List all permission groups with optional filtering. + * + * Query Parameters: + * - organizationId?: string - Filter by organization ID + * + * Response: { data: AdminPermissionGroup[], pagination: PaginationMeta } + * + * DELETE /api/v1/admin/access-control + * Delete permission groups for an organization. + * Used when an enterprise plan churns to clean up access control data. + * + * Query Parameters: + * - organizationId: string - Delete all permission groups for this organization + * + * Response: { success: true, deletedCount: number, membersRemoved: number } + */ + +import { db } from '@sim/db' +import { organization, permissionGroup, permissionGroupMember, user } from '@sim/db/schema' +import { createLogger } from '@sim/logger' +import { count, eq, inArray, sql } from 'drizzle-orm' +import { withAdminAuth } from '@/app/api/v1/admin/middleware' +import { + badRequestResponse, + internalErrorResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' + +const logger = createLogger('AdminAccessControlAPI') + +export interface AdminPermissionGroup { + id: string + organizationId: string + organizationName: string | null + name: string + description: string | null + memberCount: number + createdAt: string + createdByUserId: string + createdByEmail: string | null +} + +export const GET = withAdminAuth(async (request) => { + const url = new URL(request.url) + const organizationId = url.searchParams.get('organizationId') + + try { + const baseQuery = db + .select({ + id: permissionGroup.id, + organizationId: permissionGroup.organizationId, + organizationName: organization.name, + name: permissionGroup.name, + description: permissionGroup.description, + createdAt: permissionGroup.createdAt, + createdByUserId: permissionGroup.createdBy, + createdByEmail: user.email, + }) + .from(permissionGroup) + .leftJoin(organization, eq(permissionGroup.organizationId, organization.id)) + .leftJoin(user, eq(permissionGroup.createdBy, user.id)) + + let groups + if (organizationId) { + groups = await baseQuery.where(eq(permissionGroup.organizationId, organizationId)) + } else { + groups = await baseQuery + } + + const groupsWithCounts = await Promise.all( + groups.map(async (group) => { + const [memberCount] = await db + .select({ count: count() }) + .from(permissionGroupMember) + .where(eq(permissionGroupMember.permissionGroupId, group.id)) + + return { + id: group.id, + organizationId: group.organizationId, + organizationName: group.organizationName, + name: group.name, + description: group.description, + memberCount: memberCount?.count ?? 0, + createdAt: group.createdAt.toISOString(), + createdByUserId: group.createdByUserId, + createdByEmail: group.createdByEmail, + } as AdminPermissionGroup + }) + ) + + logger.info('Admin API: Listed permission groups', { + organizationId, + count: groupsWithCounts.length, + }) + + return singleResponse({ + data: groupsWithCounts, + pagination: { + total: groupsWithCounts.length, + limit: groupsWithCounts.length, + offset: 0, + hasMore: false, + }, + }) + } catch (error) { + logger.error('Admin API: Failed to list permission groups', { error, organizationId }) + return internalErrorResponse('Failed to list permission groups') + } +}) + +export const DELETE = withAdminAuth(async (request) => { + const url = new URL(request.url) + const organizationId = url.searchParams.get('organizationId') + const reason = url.searchParams.get('reason') || 'Enterprise plan churn cleanup' + + if (!organizationId) { + return badRequestResponse('organizationId is required') + } + + try { + const existingGroups = await db + .select({ id: permissionGroup.id }) + .from(permissionGroup) + .where(eq(permissionGroup.organizationId, organizationId)) + + if (existingGroups.length === 0) { + logger.info('Admin API: No permission groups to delete', { organizationId }) + return singleResponse({ + success: true, + deletedCount: 0, + membersRemoved: 0, + message: 'No permission groups found for the given organization', + }) + } + + const groupIds = existingGroups.map((g) => g.id) + + const [memberCountResult] = await db + .select({ count: sql`count(*)` }) + .from(permissionGroupMember) + .where(inArray(permissionGroupMember.permissionGroupId, groupIds)) + + const membersToRemove = Number(memberCountResult?.count ?? 0) + + // Members are deleted via cascade when permission groups are deleted + await db.delete(permissionGroup).where(eq(permissionGroup.organizationId, organizationId)) + + logger.info('Admin API: Deleted permission groups', { + organizationId, + deletedCount: existingGroups.length, + membersRemoved: membersToRemove, + reason, + }) + + return singleResponse({ + success: true, + deletedCount: existingGroups.length, + membersRemoved: membersToRemove, + reason, + }) + } catch (error) { + logger.error('Admin API: Failed to delete permission groups', { error, organizationId }) + return internalErrorResponse('Failed to delete permission groups') + } +}) diff --git a/apps/sim/app/api/v1/admin/index.ts b/apps/sim/app/api/v1/admin/index.ts index f41409bf90..7a48593b65 100644 --- a/apps/sim/app/api/v1/admin/index.ts +++ b/apps/sim/app/api/v1/admin/index.ts @@ -55,6 +55,10 @@ * BYOK Keys: * GET /api/v1/admin/byok - List BYOK keys (?organizationId=X or ?workspaceId=X) * DELETE /api/v1/admin/byok - Delete BYOK keys for org/workspace + * + * Access Control (Permission Groups): + * GET /api/v1/admin/access-control - List permission groups (?organizationId=X) + * DELETE /api/v1/admin/access-control - Delete permission groups for org (?organizationId=X) */ export type { AdminAuthFailure, AdminAuthResult, AdminAuthSuccess } from '@/app/api/v1/admin/auth' From 22b8528daa35921992911c5ad4a276947c44161f Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 15:53:55 -0800 Subject: [PATCH 05/14] fix not being on enterprise checks --- .../app/api/permission-groups/user/route.ts | 11 ++++++++ apps/sim/executor/utils/permission-check.ts | 6 +++++ apps/sim/lib/billing/core/subscription.ts | 27 +++++++++++++++++++ apps/sim/lib/billing/index.ts | 1 + 4 files changed, 45 insertions(+) diff --git a/apps/sim/app/api/permission-groups/user/route.ts b/apps/sim/app/api/permission-groups/user/route.ts index 10df06f239..e41c826533 100644 --- a/apps/sim/app/api/permission-groups/user/route.ts +++ b/apps/sim/app/api/permission-groups/user/route.ts @@ -3,6 +3,7 @@ import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' import { and, eq } from 'drizzle-orm' import { NextResponse } from 'next/server' import { getSession } from '@/lib/auth' +import { isOrganizationOnEnterprisePlan } from '@/lib/billing' import { parsePermissionGroupConfig } from '@/lib/permission-groups/types' export async function GET(req: Request) { @@ -29,6 +30,16 @@ export async function GET(req: Request) { return NextResponse.json({ error: 'Not a member of this organization' }, { status: 403 }) } + // Short-circuit: if org is not on enterprise plan, ignore permission configs + const isEnterprise = await isOrganizationOnEnterprisePlan(organizationId) + if (!isEnterprise) { + return NextResponse.json({ + permissionGroupId: null, + groupName: null, + config: null, + }) + } + const [groupMembership] = await db .select({ permissionGroupId: permissionGroupMember.permissionGroupId, diff --git a/apps/sim/executor/utils/permission-check.ts b/apps/sim/executor/utils/permission-check.ts index 14205c1e26..232b44940c 100644 --- a/apps/sim/executor/utils/permission-check.ts +++ b/apps/sim/executor/utils/permission-check.ts @@ -2,6 +2,7 @@ import { db } from '@sim/db' import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' +import { isOrganizationOnEnterprisePlan } from '@/lib/billing' import { type PermissionGroupConfig, parsePermissionGroupConfig, @@ -53,6 +54,11 @@ export async function getUserPermissionConfig( return null } + const isEnterprise = await isOrganizationOnEnterprisePlan(membership.organizationId) + if (!isEnterprise) { + return null + } + const [groupMembership] = await db .select({ config: permissionGroup.config }) .from(permissionGroupMember) diff --git a/apps/sim/lib/billing/core/subscription.ts b/apps/sim/lib/billing/core/subscription.ts index fdf4cd78b5..d5721b7ebd 100644 --- a/apps/sim/lib/billing/core/subscription.ts +++ b/apps/sim/lib/billing/core/subscription.ts @@ -275,6 +275,33 @@ export async function isOrganizationOnTeamOrEnterprisePlan( } } +/** + * Check if an organization has an enterprise plan + * Used for Access Control (Permission Groups) feature gating + */ +export async function isOrganizationOnEnterprisePlan(organizationId: string): Promise { + try { + if (!isProd) { + return true + } + + if (isAccessControlEnabled && !isHosted) { + return true + } + + const [orgSub] = await db + .select() + .from(subscription) + .where(and(eq(subscription.referenceId, organizationId), eq(subscription.status, 'active'))) + .limit(1) + + return !!orgSub && checkEnterprisePlan(orgSub) + } catch (error) { + logger.error('Error checking organization enterprise plan status', { error, organizationId }) + return false + } +} + /** * Check if user has access to credential sets (email polling) feature * Returns true if: diff --git a/apps/sim/lib/billing/index.ts b/apps/sim/lib/billing/index.ts index e120c0ad66..ddd4b8d1c5 100644 --- a/apps/sim/lib/billing/index.ts +++ b/apps/sim/lib/billing/index.ts @@ -15,6 +15,7 @@ export { hasSSOAccess, isEnterpriseOrgAdminOrOwner, isEnterprisePlan as hasEnterprisePlan, + isOrganizationOnEnterprisePlan, isOrganizationOnTeamOrEnterprisePlan, isProPlan as hasProPlan, isTeamOrgAdminOrOwner, From 0bf493b059bb9ac655dbde21a6a137869a92a582 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 16:18:41 -0800 Subject: [PATCH 06/14] separate out orgs from billing system --- apps/sim/app/api/v1/admin/index.ts | 1 + .../admin/organizations/[id]/billing/route.ts | 41 ++++++- .../[id]/members/[memberId]/route.ts | 3 +- .../admin/organizations/[id]/members/route.ts | 5 +- .../app/api/v1/admin/organizations/route.ts | 111 +++++++++++++++++- apps/sim/lib/auth/auth.ts | 8 ++ .../lib/billing/validation/seat-management.ts | 16 ++- apps/sim/lib/core/config/env.ts | 5 + 8 files changed, 181 insertions(+), 9 deletions(-) diff --git a/apps/sim/app/api/v1/admin/index.ts b/apps/sim/app/api/v1/admin/index.ts index 7a48593b65..ad91e0c447 100644 --- a/apps/sim/app/api/v1/admin/index.ts +++ b/apps/sim/app/api/v1/admin/index.ts @@ -36,6 +36,7 @@ * * Organizations: * GET /api/v1/admin/organizations - List all organizations + * POST /api/v1/admin/organizations - Create organization (requires ownerId) * GET /api/v1/admin/organizations/:id - Get organization details * PATCH /api/v1/admin/organizations/:id - Update organization * GET /api/v1/admin/organizations/:id/members - List organization members diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts index 952b437144..b563699830 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/billing/route.ts @@ -16,10 +16,11 @@ */ import { db } from '@sim/db' -import { organization } from '@sim/db/schema' +import { member, organization } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { eq } from 'drizzle-orm' +import { count, eq } from 'drizzle-orm' import { getOrganizationBillingData } from '@/lib/billing/core/organization' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { badRequestResponse, @@ -39,6 +40,42 @@ export const GET = withAdminAuthParams(async (_, context) => { const { id: organizationId } = await context.params try { + if (!isBillingEnabled) { + const [[orgData], [memberCount]] = await Promise.all([ + db.select().from(organization).where(eq(organization.id, organizationId)).limit(1), + db.select({ count: count() }).from(member).where(eq(member.organizationId, organizationId)), + ]) + + if (!orgData) { + return notFoundResponse('Organization') + } + + const data: AdminOrganizationBillingSummary = { + organizationId: orgData.id, + organizationName: orgData.name, + subscriptionPlan: 'none', + subscriptionStatus: 'none', + totalSeats: Number.MAX_SAFE_INTEGER, + usedSeats: memberCount?.count || 0, + availableSeats: Number.MAX_SAFE_INTEGER, + totalCurrentUsage: 0, + totalUsageLimit: Number.MAX_SAFE_INTEGER, + minimumBillingAmount: 0, + averageUsagePerMember: 0, + usagePercentage: 0, + billingPeriodStart: null, + billingPeriodEnd: null, + membersOverLimit: 0, + membersNearLimit: 0, + } + + logger.info( + `Admin API: Retrieved billing summary for organization ${organizationId} (billing disabled)` + ) + + return singleResponse(data) + } + const billingData = await getOrganizationBillingData(organizationId) if (!billingData) { diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts index 2496c363c6..d3691a6720 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/[memberId]/route.ts @@ -30,6 +30,7 @@ import { member, organization, user, userStats } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { and, eq } from 'drizzle-orm' import { removeUserFromOrganization } from '@/lib/billing/organizations/membership' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { badRequestResponse, @@ -182,7 +183,7 @@ export const PATCH = withAdminAuthParams(async (request, context) = export const DELETE = withAdminAuthParams(async (request, context) => { const { id: organizationId, memberId } = await context.params const url = new URL(request.url) - const skipBillingLogic = url.searchParams.get('skipBillingLogic') === 'true' + const skipBillingLogic = !isBillingEnabled || url.searchParams.get('skipBillingLogic') === 'true' try { const [orgData] = await db diff --git a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts index 797831b887..cc9cee6320 100644 --- a/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/[id]/members/route.ts @@ -34,6 +34,7 @@ import { createLogger } from '@sim/logger' import { count, eq } from 'drizzle-orm' import { addUserToOrganization } from '@/lib/billing/organizations/membership' import { requireStripeClient } from '@/lib/billing/stripe-client' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { withAdminAuthParams } from '@/app/api/v1/admin/middleware' import { badRequestResponse, @@ -221,14 +222,14 @@ export const POST = withAdminAuthParams(async (request, context) => userId: body.userId, organizationId, role: body.role, + skipBillingLogic: !isBillingEnabled, }) if (!result.success) { return badRequestResponse(result.error || 'Failed to add member') } - // Sync Pro subscription cancellation with Stripe (same as invitation flow) - if (result.billingActions.proSubscriptionToCancel?.stripeSubscriptionId) { + if (isBillingEnabled && result.billingActions.proSubscriptionToCancel?.stripeSubscriptionId) { try { const stripe = requireStripeClient() await stripe.subscriptions.update( diff --git a/apps/sim/app/api/v1/admin/organizations/route.ts b/apps/sim/app/api/v1/admin/organizations/route.ts index f19f822467..5cac5aba07 100644 --- a/apps/sim/app/api/v1/admin/organizations/route.ts +++ b/apps/sim/app/api/v1/admin/organizations/route.ts @@ -8,14 +8,32 @@ * - offset: number (default: 0) * * Response: AdminListResponse + * + * POST /api/v1/admin/organizations + * + * Create a new organization. + * + * Body: + * - name: string - Organization name (required) + * - slug: string - Organization slug (optional, auto-generated from name if not provided) + * - ownerId: string - User ID of the organization owner (required) + * + * Response: AdminSingleResponse */ +import { randomUUID } from 'crypto' import { db } from '@sim/db' -import { organization } from '@sim/db/schema' +import { member, organization, user } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { count } from 'drizzle-orm' +import { count, eq } from 'drizzle-orm' import { withAdminAuth } from '@/app/api/v1/admin/middleware' -import { internalErrorResponse, listResponse } from '@/app/api/v1/admin/responses' +import { + badRequestResponse, + internalErrorResponse, + listResponse, + notFoundResponse, + singleResponse, +} from '@/app/api/v1/admin/responses' import { type AdminOrganization, createPaginationMeta, @@ -47,3 +65,90 @@ export const GET = withAdminAuth(async (request) => { return internalErrorResponse('Failed to list organizations') } }) + +export const POST = withAdminAuth(async (request) => { + try { + const body = await request.json() + + if (!body.name || typeof body.name !== 'string' || body.name.trim().length === 0) { + return badRequestResponse('name is required') + } + + if (!body.ownerId || typeof body.ownerId !== 'string') { + return badRequestResponse('ownerId is required') + } + + const [ownerData] = await db + .select({ id: user.id, name: user.name }) + .from(user) + .where(eq(user.id, body.ownerId)) + .limit(1) + + if (!ownerData) { + return notFoundResponse('Owner user') + } + + const [existingMembership] = await db + .select({ organizationId: member.organizationId }) + .from(member) + .where(eq(member.userId, body.ownerId)) + .limit(1) + + if (existingMembership) { + return badRequestResponse( + 'User is already a member of another organization. Users can only belong to one organization at a time.' + ) + } + + const name = body.name.trim() + const slug = + body.slug?.trim() || + name + .toLowerCase() + .replace(/[^a-z0-9]+/g, '-') + .replace(/^-|-$/g, '') + + const organizationId = randomUUID() + const memberId = randomUUID() + const now = new Date() + + await db.transaction(async (tx) => { + await tx.insert(organization).values({ + id: organizationId, + name, + slug, + createdAt: now, + updatedAt: now, + }) + + await tx.insert(member).values({ + id: memberId, + userId: body.ownerId, + organizationId, + role: 'owner', + createdAt: now, + }) + }) + + const [createdOrg] = await db + .select() + .from(organization) + .where(eq(organization.id, organizationId)) + .limit(1) + + logger.info(`Admin API: Created organization ${organizationId}`, { + name, + slug, + ownerId: body.ownerId, + memberId, + }) + + return singleResponse({ + ...toAdminOrganization(createdOrg), + memberId, + }) + } catch (error) { + logger.error('Admin API: Failed to create organization', { error }) + return internalErrorResponse('Failed to create organization') + } +}) diff --git a/apps/sim/lib/auth/auth.ts b/apps/sim/lib/auth/auth.ts index 43e4c919ad..4956afa754 100644 --- a/apps/sim/lib/auth/auth.ts +++ b/apps/sim/lib/auth/auth.ts @@ -50,6 +50,7 @@ import { isEmailPasswordEnabled, isEmailVerificationEnabled, isHosted, + isOrganizationsEnabled, isRegistrationDisabled, } from '@/lib/core/config/feature-flags' import { PlatformEvents } from '@/lib/core/telemetry' @@ -2324,8 +2325,15 @@ export const auth = betterAuth({ } }, }), + ] + : []), + ...(isOrganizationsEnabled + ? [ organization({ allowUserToCreateOrganization: async (user) => { + if (!isBillingEnabled) { + return true + } const dbSubscriptions = await db .select() .from(schema.subscription) diff --git a/apps/sim/lib/billing/validation/seat-management.ts b/apps/sim/lib/billing/validation/seat-management.ts index 250514a00a..58292f3905 100644 --- a/apps/sim/lib/billing/validation/seat-management.ts +++ b/apps/sim/lib/billing/validation/seat-management.ts @@ -4,6 +4,7 @@ import { createLogger } from '@sim/logger' import { and, count, eq } from 'drizzle-orm' import { getOrganizationSubscription } from '@/lib/billing/core/billing' import { getEffectiveSeats } from '@/lib/billing/subscriptions/utils' +import { isBillingEnabled } from '@/lib/core/config/feature-flags' import { quickValidateEmail } from '@/lib/messaging/email/validation' const logger = createLogger('SeatManagement') @@ -34,7 +35,20 @@ export async function validateSeatAvailability( additionalSeats = 1 ): Promise { try { - // Get organization subscription directly (referenceId = organizationId) + if (!isBillingEnabled) { + const memberCount = await db + .select({ count: count() }) + .from(member) + .where(eq(member.organizationId, organizationId)) + const currentSeats = memberCount[0]?.count || 0 + return { + canInvite: true, + currentSeats, + maxSeats: Number.MAX_SAFE_INTEGER, + availableSeats: Number.MAX_SAFE_INTEGER, + } + } + const subscription = await getOrganizationSubscription(organizationId) if (!subscription) { diff --git a/apps/sim/lib/core/config/env.ts b/apps/sim/lib/core/config/env.ts index d893bb0185..c2be459174 100644 --- a/apps/sim/lib/core/config/env.ts +++ b/apps/sim/lib/core/config/env.ts @@ -254,6 +254,9 @@ export const env = createEnv({ // Access Control (Permission Groups) - for self-hosted deployments ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control on self-hosted (bypasses plan requirements) + // Organizations - for self-hosted deployments + ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements) + // SSO Configuration (for script-based registration) SSO_ENABLED: z.boolean().optional(), // Enable SSO functionality SSO_PROVIDER_TYPE: z.enum(['oidc', 'saml']).optional(), // [REQUIRED] SSO provider type @@ -333,6 +336,7 @@ export const env = createEnv({ NEXT_PUBLIC_SSO_ENABLED: z.boolean().optional(), // Enable SSO login UI components NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED: z.boolean().optional(), // Enable credential sets (email polling) on self-hosted NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: z.boolean().optional(), // Enable access control (permission groups) on self-hosted + NEXT_PUBLIC_ORGANIZATIONS_ENABLED: z.boolean().optional(), // Enable organizations on self-hosted (bypasses plan requirements) NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: z.boolean().optional().default(true), // Control visibility of email/password login forms }, @@ -363,6 +367,7 @@ export const env = createEnv({ NEXT_PUBLIC_SSO_ENABLED: process.env.NEXT_PUBLIC_SSO_ENABLED, NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED: process.env.NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED, NEXT_PUBLIC_ACCESS_CONTROL_ENABLED: process.env.NEXT_PUBLIC_ACCESS_CONTROL_ENABLED, + NEXT_PUBLIC_ORGANIZATIONS_ENABLED: process.env.NEXT_PUBLIC_ORGANIZATIONS_ENABLED, NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED: process.env.NEXT_PUBLIC_EMAIL_PASSWORD_SIGNUP_ENABLED, NEXT_PUBLIC_E2B_ENABLED: process.env.NEXT_PUBLIC_E2B_ENABLED, NEXT_PUBLIC_COPILOT_TRAINING_ENABLED: process.env.NEXT_PUBLIC_COPILOT_TRAINING_ENABLED, From e2366b19e5c1b14f6e8b8c2211e448dd225139d6 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 16:39:19 -0800 Subject: [PATCH 07/14] update the docs --- .../docs/content/docs/en/enterprise/index.mdx | 55 +++++++++++++++++-- .../settings-modal/settings-modal.tsx | 9 +-- 2 files changed, 55 insertions(+), 9 deletions(-) diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx index c5b451d83d..f35e54bd0f 100644 --- a/apps/docs/content/docs/en/enterprise/index.mdx +++ b/apps/docs/content/docs/en/enterprise/index.mdx @@ -9,6 +9,28 @@ Sim Studio Enterprise provides advanced features for organizations with enhanced --- +## Access Control + +Define permission groups to control what features and integrations team members can use. + +### Features + +- **Allowed Model Providers** - Restrict which AI providers users can access (OpenAI, Anthropic, Google, etc.) +- **Allowed Blocks** - Control which workflow blocks are available +- **Platform Settings** - Hide Knowledge Base, disable MCP tools, or disable custom tools + +### Setup + +1. Navigate to **Settings** → **Access Control** in your workspace +2. Create a permission group with your desired restrictions +3. Add team members to the permission group + + + Users not assigned to any permission group have full access. Permission restrictions are enforced at both UI and execution time. + + +--- + ## Bring Your Own Key (BYOK) Use your own API keys for AI model providers instead of Sim Studio's hosted keys. @@ -61,15 +83,38 @@ Enterprise authentication with SAML 2.0 and OIDC support for centralized identit --- -## Self-Hosted +## Self-Hosted Configuration + +For self-hosted deployments, enterprise features can be enabled via environment variables without requiring billing. -For self-hosted deployments, enterprise features can be enabled via environment variables: +### Environment Variables | Variable | Description | |----------|-------------| +| `ORGANIZATIONS_ENABLED`, `NEXT_PUBLIC_ORGANIZATIONS_ENABLED` | Enable team/organization management | +| `ACCESS_CONTROL_ENABLED`, `NEXT_PUBLIC_ACCESS_CONTROL_ENABLED` | Permission groups for access restrictions | | `SSO_ENABLED`, `NEXT_PUBLIC_SSO_ENABLED` | Single Sign-On with SAML/OIDC | | `CREDENTIAL_SETS_ENABLED`, `NEXT_PUBLIC_CREDENTIAL_SETS_ENABLED` | Polling Groups for email triggers | - - BYOK is only available on hosted Sim Studio. Self-hosted deployments configure AI provider keys directly via environment variables. - +### Organization Management + +When billing is disabled, use the Admin API to manage organizations: + +```bash +# Create an organization +curl -X POST https://your-instance/api/v1/admin/organizations \ + -H "x-admin-key: YOUR_ADMIN_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"name": "My Organization", "ownerId": "user-id-here"}' + +# Add a member +curl -X POST https://your-instance/api/v1/admin/organizations/{orgId}/members \ + -H "x-admin-key: YOUR_ADMIN_API_KEY" \ + -H "Content-Type: application/json" \ + -d '{"userId": "user-id-here", "role": "admin"}' +``` + +### Notes + +- Enabling `ACCESS_CONTROL_ENABLED` automatically enables organizations, as access control requires organization membership. +- BYOK is only available on hosted Sim Studio. Self-hosted deployments configure AI provider keys directly via environment variables. diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx index 066e5d26a7..8ea8db7225 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/settings-modal.tsx @@ -92,7 +92,7 @@ type SettingsSection = | 'custom-tools' | 'workflow-mcp-servers' -type NavigationSection = 'account' | 'subscription' | 'tools' | 'system' +type NavigationSection = 'account' | 'subscription' | 'tools' | 'system' | 'enterprise' type NavigationItem = { id: SettingsSection @@ -111,6 +111,7 @@ const sectionConfig: { key: NavigationSection; title: string }[] = [ { key: 'tools', title: 'Tools' }, { key: 'subscription', title: 'Subscription' }, { key: 'system', title: 'System' }, + { key: 'enterprise', title: 'Enterprise' }, ] const allNavigationItems: NavigationItem[] = [ @@ -120,7 +121,7 @@ const allNavigationItems: NavigationItem[] = [ id: 'access-control', label: 'Access Control', icon: ShieldCheck, - section: 'account', + section: 'enterprise', requiresHosted: true, requiresEnterprise: true, selfHostedOverride: isAccessControlEnabled, @@ -159,7 +160,7 @@ const allNavigationItems: NavigationItem[] = [ id: 'byok', label: 'BYOK', icon: KeySquare, - section: 'system', + section: 'enterprise', requiresHosted: true, requiresEnterprise: true, }, @@ -175,7 +176,7 @@ const allNavigationItems: NavigationItem[] = [ id: 'sso', label: 'Single Sign-On', icon: LogIn, - section: 'system', + section: 'enterprise', requiresHosted: true, requiresEnterprise: true, selfHostedOverride: isSSOEnabled, From 87f0f3fe11e2526bba670ddee90b08afd8ca6b18 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 17:26:45 -0800 Subject: [PATCH 08/14] add custom tool blockers based on perm configs --- .../docs/content/docs/en/enterprise/index.mdx | 2 +- .../access-control/access-control.tsx | 55 ++++------ .../client/workflow/manage-custom-tool.ts | 22 +++- .../tools/client/workflow/manage-mcp-tool.ts | 22 +++- .../tools/server/workflow/edit-workflow.ts | 101 ++++++++++++++++-- apps/sim/lib/core/config/feature-flags.ts | 8 ++ 6 files changed, 161 insertions(+), 49 deletions(-) diff --git a/apps/docs/content/docs/en/enterprise/index.mdx b/apps/docs/content/docs/en/enterprise/index.mdx index f35e54bd0f..3e5acdf5e2 100644 --- a/apps/docs/content/docs/en/enterprise/index.mdx +++ b/apps/docs/content/docs/en/enterprise/index.mdx @@ -1,6 +1,6 @@ --- title: Enterprise -description: Enterprise features for organizations with advanced security and compliance requirements +description: Enterprise features for business organizations --- import { Callout } from 'fumadocs-ui/components/callout' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx index 442984fb3d..44beda97d5 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' -import { Check, Plus, Search, Users } from 'lucide-react' +import { Check, Plus, Search } from 'lucide-react' import { Avatar, AvatarFallback, @@ -864,40 +864,25 @@ export function AccessControl() {

) : (
- +
+ +
{availableMembersToAdd.map((member: any) => { const name = member.user?.name || 'Unknown' const email = member.user?.email || '' diff --git a/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts b/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts index 9918b5c7b5..dfc9bcaa02 100644 --- a/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts +++ b/apps/sim/lib/copilot/tools/client/workflow/manage-custom-tool.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { Check, Loader2, Plus, X, XCircle } from 'lucide-react' +import { client } from '@/lib/auth/auth-client' import { BaseClientTool, type BaseClientToolMetadata, @@ -31,6 +32,20 @@ interface ManageCustomToolArgs { const API_ENDPOINT = '/api/tools/custom' +async function checkCustomToolsPermission(): Promise { + const activeOrgResponse = await client.organization.getFullOrganization() + const organizationId = activeOrgResponse.data?.id + if (!organizationId) return + + const response = await fetch(`/api/permission-groups/user?organizationId=${organizationId}`) + if (!response.ok) return + + const data = await response.json() + if (data?.config?.disableCustomTools) { + throw new Error('Custom tools are not allowed based on your permission group settings') + } +} + /** * Client tool for creating, editing, and deleting custom tools via the copilot. */ @@ -164,7 +179,10 @@ export class ManageCustomToolClientTool extends BaseClientTool { } catch (e: any) { logger.error('execute failed', { message: e?.message }) this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to manage custom tool') + await this.markToolComplete(500, e?.message || 'Failed to manage custom tool', { + success: false, + error: e?.message || 'Failed to manage custom tool', + }) } } @@ -189,6 +207,8 @@ export class ManageCustomToolClientTool extends BaseClientTool { throw new Error('Operation is required') } + await checkCustomToolsPermission() + const { operation, toolId, schema, code } = args // Get workspace ID from the workflow registry diff --git a/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts b/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts index 3f16d3c1e9..796574dc1b 100644 --- a/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts +++ b/apps/sim/lib/copilot/tools/client/workflow/manage-mcp-tool.ts @@ -1,5 +1,6 @@ import { createLogger } from '@sim/logger' import { Check, Loader2, Server, X, XCircle } from 'lucide-react' +import { client } from '@/lib/auth/auth-client' import { BaseClientTool, type BaseClientToolMetadata, @@ -25,6 +26,20 @@ interface ManageMcpToolArgs { const API_ENDPOINT = '/api/mcp/servers' +async function checkMcpToolsPermission(): Promise { + const activeOrgResponse = await client.organization.getFullOrganization() + const organizationId = activeOrgResponse.data?.id + if (!organizationId) return + + const response = await fetch(`/api/permission-groups/user?organizationId=${organizationId}`) + if (!response.ok) return + + const data = await response.json() + if (data?.config?.disableMcpTools) { + throw new Error('MCP tools are not allowed based on your permission group settings') + } +} + /** * Client tool for creating, editing, and deleting MCP tool servers via the copilot. */ @@ -145,7 +160,10 @@ export class ManageMcpToolClientTool extends BaseClientTool { } catch (e: any) { logger.error('execute failed', { message: e?.message }) this.setState(ClientToolCallState.error) - await this.markToolComplete(500, e?.message || 'Failed to manage MCP tool') + await this.markToolComplete(500, e?.message || 'Failed to manage MCP tool', { + success: false, + error: e?.message || 'Failed to manage MCP tool', + }) } } @@ -167,6 +185,8 @@ export class ManageMcpToolClientTool extends BaseClientTool { throw new Error('Operation is required') } + await checkMcpToolsPermission() + const { operation, serverId, config } = args const { hydration } = useWorkflowRegistry.getState() diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 4a7d69ee72..127ec1029b 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -52,6 +52,7 @@ type SkippedItemType = | 'block_not_found' | 'invalid_block_type' | 'block_not_allowed' + | 'tool_not_allowed' | 'invalid_edge_target' | 'invalid_edge_source' | 'invalid_source_handle' @@ -561,7 +562,9 @@ function createBlockFromParams( blockId: string, params: any, parentId?: string, - errorsCollector?: ValidationError[] + errorsCollector?: ValidationError[], + permissionConfig?: PermissionGroupConfig | null, + skippedItems?: SkippedItem[] ): any { const blockConfig = getAllBlocks().find((b) => b.type === params.type) @@ -629,9 +632,14 @@ function createBlockFromParams( } } - // Special handling for tools - normalize to restore sanitized fields + // Special handling for tools - normalize and filter disallowed if (key === 'tools' && Array.isArray(value)) { - sanitizedValue = normalizeTools(value) + sanitizedValue = filterDisallowedTools( + normalizeTools(value), + permissionConfig ?? null, + blockId, + skippedItems ?? [] + ) } // Special handling for responseFormat - normalize to ensure consistent format @@ -1109,6 +1117,49 @@ function isBlockTypeAllowed( return permissionConfig.allowedIntegrations.includes(blockType) } +/** + * Filters out tools that are not allowed by the permission group config + * Returns both the allowed tools and any skipped tool items for logging + */ +function filterDisallowedTools( + tools: any[], + permissionConfig: PermissionGroupConfig | null, + blockId: string, + skippedItems: SkippedItem[] +): any[] { + if (!permissionConfig) { + return tools + } + + const allowedTools: any[] = [] + + for (const tool of tools) { + if (tool.type === 'custom-tool' && permissionConfig.disableCustomTools) { + logSkippedItem(skippedItems, { + type: 'tool_not_allowed', + operationType: 'add', + blockId, + reason: `Custom tool "${tool.title || tool.customToolId || 'unknown'}" is not allowed by permission group - tool not added`, + details: { toolType: 'custom-tool', toolId: tool.customToolId }, + }) + continue + } + if (tool.type === 'mcp' && permissionConfig.disableMcpTools) { + logSkippedItem(skippedItems, { + type: 'tool_not_allowed', + operationType: 'add', + blockId, + reason: `MCP tool "${tool.title || 'unknown'}" is not allowed by permission group - tool not added`, + details: { toolType: 'mcp', serverId: tool.params?.serverId }, + }) + continue + } + allowedTools.push(tool) + } + + return allowedTools +} + /** * Apply operations directly to the workflow JSON state */ @@ -1314,9 +1365,14 @@ function applyOperationsToWorkflowState( } } - // Special handling for tools - normalize to restore sanitized fields + // Special handling for tools - normalize and filter disallowed if (key === 'tools' && Array.isArray(value)) { - sanitizedValue = normalizeTools(value) + sanitizedValue = filterDisallowedTools( + normalizeTools(value), + permissionConfig, + block_id, + skippedItems + ) } // Special handling for responseFormat - normalize to ensure consistent format @@ -1528,7 +1584,9 @@ function applyOperationsToWorkflowState( childId, childBlock, block_id, - validationErrors + validationErrors, + permissionConfig, + skippedItems ) modifiedState.blocks[childId] = childBlockState @@ -1718,7 +1776,14 @@ function applyOperationsToWorkflowState( } // Create new block with proper structure - const newBlock = createBlockFromParams(block_id, params, undefined, validationErrors) + const newBlock = createBlockFromParams( + block_id, + params, + undefined, + validationErrors, + permissionConfig, + skippedItems + ) // Set loop/parallel data on parent block BEFORE adding to blocks (strict validation) if (params.nestedNodes) { @@ -1797,7 +1862,9 @@ function applyOperationsToWorkflowState( childId, childBlock, block_id, - validationErrors + validationErrors, + permissionConfig, + skippedItems ) modifiedState.blocks[childId] = childBlockState @@ -1919,9 +1986,14 @@ function applyOperationsToWorkflowState( } } - // Special handling for tools - normalize to restore sanitized fields + // Special handling for tools - normalize and filter disallowed if (key === 'tools' && Array.isArray(value)) { - sanitizedValue = normalizeTools(value) + sanitizedValue = filterDisallowedTools( + normalizeTools(value), + permissionConfig, + block_id, + skippedItems + ) } // Special handling for responseFormat - normalize to ensure consistent format @@ -1970,7 +2042,14 @@ function applyOperationsToWorkflowState( } // Create new block as child of subflow - const newBlock = createBlockFromParams(block_id, params, subflowId, validationErrors) + const newBlock = createBlockFromParams( + block_id, + params, + subflowId, + validationErrors, + permissionConfig, + skippedItems + ) modifiedState.blocks[block_id] = newBlock } diff --git a/apps/sim/lib/core/config/feature-flags.ts b/apps/sim/lib/core/config/feature-flags.ts index cf17b0e9f5..5e7f26d0d0 100644 --- a/apps/sim/lib/core/config/feature-flags.ts +++ b/apps/sim/lib/core/config/feature-flags.ts @@ -92,6 +92,14 @@ export const isCredentialSetsEnabled = isTruthy(env.CREDENTIAL_SETS_ENABLED) */ export const isAccessControlEnabled = isTruthy(env.ACCESS_CONTROL_ENABLED) +/** + * Is organizations enabled + * True if billing is enabled (orgs come with billing), OR explicitly enabled via env var, + * OR if access control is enabled (access control requires organizations) + */ +export const isOrganizationsEnabled = + isBillingEnabled || isTruthy(env.ORGANIZATIONS_ENABLED) || isAccessControlEnabled + /** * Is E2B enabled for remote code execution */ From 79befdf7e21901da7aa655fc6f0061192711d923 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 17:35:03 -0800 Subject: [PATCH 09/14] add migrations --- packages/db/migrations/0137_absurd_sumo.sql | 30 + .../db/migrations/meta/0137_snapshot.json | 9592 +++++++++++++++++ packages/db/migrations/meta/_journal.json | 7 + 3 files changed, 9629 insertions(+) create mode 100644 packages/db/migrations/0137_absurd_sumo.sql create mode 100644 packages/db/migrations/meta/0137_snapshot.json diff --git a/packages/db/migrations/0137_absurd_sumo.sql b/packages/db/migrations/0137_absurd_sumo.sql new file mode 100644 index 0000000000..8f85a66b71 --- /dev/null +++ b/packages/db/migrations/0137_absurd_sumo.sql @@ -0,0 +1,30 @@ +CREATE TABLE "permission_group" ( + "id" text PRIMARY KEY NOT NULL, + "organization_id" text NOT NULL, + "name" text NOT NULL, + "description" text, + "config" jsonb DEFAULT '{}' NOT NULL, + "created_by" text NOT NULL, + "created_at" timestamp DEFAULT now() NOT NULL, + "updated_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "permission_group_member" ( + "id" text PRIMARY KEY NOT NULL, + "permission_group_id" text NOT NULL, + "user_id" text NOT NULL, + "assigned_by" text, + "assigned_at" timestamp DEFAULT now() NOT NULL +); +--> statement-breakpoint +ALTER TABLE "permission_group" ADD CONSTRAINT "permission_group_organization_id_organization_id_fk" FOREIGN KEY ("organization_id") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "permission_group" ADD CONSTRAINT "permission_group_created_by_user_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "permission_group_member" ADD CONSTRAINT "permission_group_member_permission_group_id_permission_group_id_fk" FOREIGN KEY ("permission_group_id") REFERENCES "public"."permission_group"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "permission_group_member" ADD CONSTRAINT "permission_group_member_user_id_user_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint +ALTER TABLE "permission_group_member" ADD CONSTRAINT "permission_group_member_assigned_by_user_id_fk" FOREIGN KEY ("assigned_by") REFERENCES "public"."user"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint +CREATE INDEX "permission_group_organization_id_idx" ON "permission_group" USING btree ("organization_id");--> statement-breakpoint +CREATE INDEX "permission_group_created_by_idx" ON "permission_group" USING btree ("created_by");--> statement-breakpoint +CREATE UNIQUE INDEX "permission_group_org_name_unique" ON "permission_group" USING btree ("organization_id","name");--> statement-breakpoint +CREATE INDEX "permission_group_member_group_id_idx" ON "permission_group_member" USING btree ("permission_group_id");--> statement-breakpoint +CREATE INDEX "permission_group_member_user_id_idx" ON "permission_group_member" USING btree ("user_id");--> statement-breakpoint +CREATE UNIQUE INDEX "permission_group_member_unique" ON "permission_group_member" USING btree ("permission_group_id","user_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0137_snapshot.json b/packages/db/migrations/meta/0137_snapshot.json new file mode 100644 index 0000000000..93a3b83f9a --- /dev/null +++ b/packages/db/migrations/meta/0137_snapshot.json @@ -0,0 +1,9592 @@ +{ + "id": "60200982-7693-47b9-a396-c2ffad54f529", + "prevId": "ff6b124c-50b5-4e92-bbe3-0b162a184d5b", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.account": { + "name": "account", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "account_id": { + "name": "account_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refresh_token": { + "name": "refresh_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "id_token": { + "name": "id_token", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "access_token_expires_at": { + "name": "access_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "refresh_token_expires_at": { + "name": "refresh_token_expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "account_user_id_idx": { + "name": "account_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_account_on_account_id_provider_id": { + "name": "idx_account_on_account_id_provider_id", + "columns": [ + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "account_user_provider_account_unique": { + "name": "account_user_provider_account_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "account_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "account_user_id_user_id_fk": { + "name": "account_user_id_user_id_fk", + "tableFrom": "account", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.api_key": { + "name": "api_key", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'personal'" + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "api_key_workspace_type_idx": { + "name": "api_key_workspace_type_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "api_key_user_type_idx": { + "name": "api_key_user_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "api_key_user_id_user_id_fk": { + "name": "api_key_user_id_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_workspace_id_workspace_id_fk": { + "name": "api_key_workspace_id_workspace_id_fk", + "tableFrom": "api_key", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "api_key_created_by_user_id_fk": { + "name": "api_key_created_by_user_id_fk", + "tableFrom": "api_key", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "api_key_key_unique": { + "name": "api_key_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": { + "workspace_type_check": { + "name": "workspace_type_check", + "value": "(type = 'workspace' AND workspace_id IS NOT NULL) OR (type = 'personal' AND workspace_id IS NULL)" + } + }, + "isRLSEnabled": false + }, + "public.chat": { + "name": "chat", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "customizations": { + "name": "customizations", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "auth_type": { + "name": "auth_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'public'" + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "allowed_emails": { + "name": "allowed_emails", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "output_configs": { + "name": "output_configs", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'[]'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "identifier_idx": { + "name": "identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "chat_workflow_id_workflow_id_fk": { + "name": "chat_workflow_id_workflow_id_fk", + "tableFrom": "chat", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "chat_user_id_user_id_fk": { + "name": "chat_user_id_user_id_fk", + "tableFrom": "chat", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_chats": { + "name": "copilot_chats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "messages": { + "name": "messages", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "model": { + "name": "model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'claude-3-7-sonnet-latest'" + }, + "conversation_id": { + "name": "conversation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "preview_yaml": { + "name": "preview_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "plan_artifact": { + "name": "plan_artifact", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_chats_user_id_idx": { + "name": "copilot_chats_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_workflow_id_idx": { + "name": "copilot_chats_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_user_workflow_idx": { + "name": "copilot_chats_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_created_at_idx": { + "name": "copilot_chats_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_chats_updated_at_idx": { + "name": "copilot_chats_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_chats_user_id_user_id_fk": { + "name": "copilot_chats_user_id_user_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_chats_workflow_id_workflow_id_fk": { + "name": "copilot_chats_workflow_id_workflow_id_fk", + "tableFrom": "copilot_chats", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.copilot_feedback": { + "name": "copilot_feedback", + "schema": "", + "columns": { + "feedback_id": { + "name": "feedback_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "user_query": { + "name": "user_query", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "agent_response": { + "name": "agent_response", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "is_positive": { + "name": "is_positive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "feedback": { + "name": "feedback", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_yaml": { + "name": "workflow_yaml", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "copilot_feedback_user_id_idx": { + "name": "copilot_feedback_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_chat_id_idx": { + "name": "copilot_feedback_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_user_chat_idx": { + "name": "copilot_feedback_user_chat_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_is_positive_idx": { + "name": "copilot_feedback_is_positive_idx", + "columns": [ + { + "expression": "is_positive", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "copilot_feedback_created_at_idx": { + "name": "copilot_feedback_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "copilot_feedback_user_id_user_id_fk": { + "name": "copilot_feedback_user_id_user_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "copilot_feedback_chat_id_copilot_chats_id_fk": { + "name": "copilot_feedback_chat_id_copilot_chats_id_fk", + "tableFrom": "copilot_feedback", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set": { + "name": "credential_set", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_organization_id_idx": { + "name": "credential_set_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_created_by_idx": { + "name": "credential_set_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_org_name_unique": { + "name": "credential_set_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_provider_id_idx": { + "name": "credential_set_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_organization_id_organization_id_fk": { + "name": "credential_set_organization_id_organization_id_fk", + "tableFrom": "credential_set", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_created_by_user_id_fk": { + "name": "credential_set_created_by_user_id_fk", + "tableFrom": "credential_set", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_invitation": { + "name": "credential_set_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "accepted_at": { + "name": "accepted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "accepted_by_user_id": { + "name": "accepted_by_user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_invitation_set_id_idx": { + "name": "credential_set_invitation_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_token_idx": { + "name": "credential_set_invitation_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_status_idx": { + "name": "credential_set_invitation_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_invitation_expires_at_idx": { + "name": "credential_set_invitation_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_invitation_credential_set_id_credential_set_id_fk": { + "name": "credential_set_invitation_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_invited_by_user_id_fk": { + "name": "credential_set_invitation_invited_by_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_invitation_accepted_by_user_id_user_id_fk": { + "name": "credential_set_invitation_accepted_by_user_id_user_id_fk", + "tableFrom": "credential_set_invitation", + "tableTo": "user", + "columnsFrom": ["accepted_by_user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "credential_set_invitation_token_unique": { + "name": "credential_set_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.credential_set_member": { + "name": "credential_set_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "credential_set_member_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "joined_at": { + "name": "joined_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "invited_by": { + "name": "invited_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "credential_set_member_set_id_idx": { + "name": "credential_set_member_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_user_id_idx": { + "name": "credential_set_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_unique": { + "name": "credential_set_member_unique", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "credential_set_member_status_idx": { + "name": "credential_set_member_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "credential_set_member_credential_set_id_credential_set_id_fk": { + "name": "credential_set_member_credential_set_id_credential_set_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_user_id_user_id_fk": { + "name": "credential_set_member_user_id_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "credential_set_member_invited_by_user_id_fk": { + "name": "credential_set_member_invited_by_user_id_fk", + "tableFrom": "credential_set_member", + "tableTo": "user", + "columnsFrom": ["invited_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.custom_tools": { + "name": "custom_tools", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "schema": { + "name": "schema", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "custom_tools_workspace_id_idx": { + "name": "custom_tools_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "custom_tools_workspace_title_unique": { + "name": "custom_tools_workspace_title_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "title", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "custom_tools_workspace_id_workspace_id_fk": { + "name": "custom_tools_workspace_id_workspace_id_fk", + "tableFrom": "custom_tools", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "custom_tools_user_id_user_id_fk": { + "name": "custom_tools_user_id_user_id_fk", + "tableFrom": "custom_tools", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.docs_embeddings": { + "name": "docs_embeddings", + "schema": "", + "columns": { + "chunk_id": { + "name": "chunk_id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "chunk_text": { + "name": "chunk_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_document": { + "name": "source_document", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_link": { + "name": "source_link", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_text": { + "name": "header_text", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header_level": { + "name": "header_level", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": true + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "chunk_text_tsv": { + "name": "chunk_text_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"docs_embeddings\".\"chunk_text\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "docs_emb_source_document_idx": { + "name": "docs_emb_source_document_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_header_level_idx": { + "name": "docs_emb_header_level_idx", + "columns": [ + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_source_header_idx": { + "name": "docs_emb_source_header_idx", + "columns": [ + { + "expression": "source_document", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "header_level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_model_idx": { + "name": "docs_emb_model_idx", + "columns": [ + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_emb_created_at_idx": { + "name": "docs_emb_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "docs_embedding_vector_hnsw_idx": { + "name": "docs_embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "docs_emb_metadata_gin_idx": { + "name": "docs_emb_metadata_gin_idx", + "columns": [ + { + "expression": "metadata", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + }, + "docs_emb_chunk_text_fts_idx": { + "name": "docs_emb_chunk_text_fts_idx", + "columns": [ + { + "expression": "chunk_text_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "docs_embedding_not_null_check": { + "name": "docs_embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + }, + "docs_header_level_check": { + "name": "docs_header_level_check", + "value": "\"header_level\" >= 1 AND \"header_level\" <= 6" + } + }, + "isRLSEnabled": false + }, + "public.document": { + "name": "document", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filename": { + "name": "filename", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_url": { + "name": "file_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "file_size": { + "name": "file_size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_count": { + "name": "chunk_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "character_count": { + "name": "character_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "processing_status": { + "name": "processing_status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "processing_started_at": { + "name": "processing_started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_completed_at": { + "name": "processing_completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "processing_error": { + "name": "processing_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "doc_kb_id_idx": { + "name": "doc_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_filename_idx": { + "name": "doc_filename_idx", + "columns": [ + { + "expression": "filename", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_processing_status_idx": { + "name": "doc_processing_status_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "processing_status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag1_idx": { + "name": "doc_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag2_idx": { + "name": "doc_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag3_idx": { + "name": "doc_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag4_idx": { + "name": "doc_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag5_idx": { + "name": "doc_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag6_idx": { + "name": "doc_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_tag7_idx": { + "name": "doc_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number1_idx": { + "name": "doc_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number2_idx": { + "name": "doc_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number3_idx": { + "name": "doc_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number4_idx": { + "name": "doc_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_number5_idx": { + "name": "doc_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date1_idx": { + "name": "doc_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_date2_idx": { + "name": "doc_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean1_idx": { + "name": "doc_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean2_idx": { + "name": "doc_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "doc_boolean3_idx": { + "name": "doc_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "document_knowledge_base_id_knowledge_base_id_fk": { + "name": "document_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "document", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.embedding": { + "name": "embedding", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "document_id": { + "name": "document_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chunk_index": { + "name": "chunk_index", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "chunk_hash": { + "name": "chunk_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_length": { + "name": "content_length", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "embedding": { + "name": "embedding", + "type": "vector(1536)", + "primaryKey": false, + "notNull": false + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "start_offset": { + "name": "start_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "end_offset": { + "name": "end_offset", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "tag1": { + "name": "tag1", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag2": { + "name": "tag2", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag3": { + "name": "tag3", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag4": { + "name": "tag4", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag5": { + "name": "tag5", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag6": { + "name": "tag6", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "tag7": { + "name": "tag7", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "number1": { + "name": "number1", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number2": { + "name": "number2", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number3": { + "name": "number3", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number4": { + "name": "number4", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "number5": { + "name": "number5", + "type": "double precision", + "primaryKey": false, + "notNull": false + }, + "date1": { + "name": "date1", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "date2": { + "name": "date2", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "boolean1": { + "name": "boolean1", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean2": { + "name": "boolean2", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "boolean3": { + "name": "boolean3", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "content_tsv": { + "name": "content_tsv", + "type": "tsvector", + "primaryKey": false, + "notNull": false, + "generated": { + "as": "to_tsvector('english', \"embedding\".\"content\")", + "type": "stored" + } + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "emb_kb_id_idx": { + "name": "emb_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_id_idx": { + "name": "emb_doc_id_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_chunk_idx": { + "name": "emb_doc_chunk_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chunk_index", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_model_idx": { + "name": "emb_kb_model_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "embedding_model", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_kb_enabled_idx": { + "name": "emb_kb_enabled_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_doc_enabled_idx": { + "name": "emb_doc_enabled_idx", + "columns": [ + { + "expression": "document_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "embedding_vector_hnsw_idx": { + "name": "embedding_vector_hnsw_idx", + "columns": [ + { + "expression": "embedding", + "isExpression": false, + "asc": true, + "nulls": "last", + "opclass": "vector_cosine_ops" + } + ], + "isUnique": false, + "concurrently": false, + "method": "hnsw", + "with": { + "m": 16, + "ef_construction": 64 + } + }, + "emb_tag1_idx": { + "name": "emb_tag1_idx", + "columns": [ + { + "expression": "tag1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag2_idx": { + "name": "emb_tag2_idx", + "columns": [ + { + "expression": "tag2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag3_idx": { + "name": "emb_tag3_idx", + "columns": [ + { + "expression": "tag3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag4_idx": { + "name": "emb_tag4_idx", + "columns": [ + { + "expression": "tag4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag5_idx": { + "name": "emb_tag5_idx", + "columns": [ + { + "expression": "tag5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag6_idx": { + "name": "emb_tag6_idx", + "columns": [ + { + "expression": "tag6", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_tag7_idx": { + "name": "emb_tag7_idx", + "columns": [ + { + "expression": "tag7", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number1_idx": { + "name": "emb_number1_idx", + "columns": [ + { + "expression": "number1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number2_idx": { + "name": "emb_number2_idx", + "columns": [ + { + "expression": "number2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number3_idx": { + "name": "emb_number3_idx", + "columns": [ + { + "expression": "number3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number4_idx": { + "name": "emb_number4_idx", + "columns": [ + { + "expression": "number4", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_number5_idx": { + "name": "emb_number5_idx", + "columns": [ + { + "expression": "number5", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date1_idx": { + "name": "emb_date1_idx", + "columns": [ + { + "expression": "date1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_date2_idx": { + "name": "emb_date2_idx", + "columns": [ + { + "expression": "date2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean1_idx": { + "name": "emb_boolean1_idx", + "columns": [ + { + "expression": "boolean1", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean2_idx": { + "name": "emb_boolean2_idx", + "columns": [ + { + "expression": "boolean2", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_boolean3_idx": { + "name": "emb_boolean3_idx", + "columns": [ + { + "expression": "boolean3", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "emb_content_fts_idx": { + "name": "emb_content_fts_idx", + "columns": [ + { + "expression": "content_tsv", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "gin", + "with": {} + } + }, + "foreignKeys": { + "embedding_knowledge_base_id_knowledge_base_id_fk": { + "name": "embedding_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "embedding", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "embedding_document_id_document_id_fk": { + "name": "embedding_document_id_document_id_fk", + "tableFrom": "embedding", + "tableTo": "document", + "columnsFrom": ["document_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "embedding_not_null_check": { + "name": "embedding_not_null_check", + "value": "\"embedding\" IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.environment": { + "name": "environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "environment_user_id_user_id_fk": { + "name": "environment_user_id_user_id_fk", + "tableFrom": "environment", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "environment_user_id_unique": { + "name": "environment_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.idempotency_key": { + "name": "idempotency_key", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "namespace": { + "name": "namespace", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'default'" + }, + "result": { + "name": "result", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "idempotency_key_namespace_unique": { + "name": "idempotency_key_namespace_unique", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_created_at_idx": { + "name": "idempotency_key_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idempotency_key_namespace_idx": { + "name": "idempotency_key_namespace_idx", + "columns": [ + { + "expression": "namespace", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.invitation": { + "name": "invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "invitation_email_idx": { + "name": "invitation_email_idx", + "columns": [ + { + "expression": "email", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "invitation_organization_id_idx": { + "name": "invitation_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "invitation_inviter_id_user_id_fk": { + "name": "invitation_inviter_id_user_id_fk", + "tableFrom": "invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "invitation_organization_id_organization_id_fk": { + "name": "invitation_organization_id_organization_id_fk", + "tableFrom": "invitation", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base": { + "name": "knowledge_base", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "token_count": { + "name": "token_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "embedding_model": { + "name": "embedding_model", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text-embedding-3-small'" + }, + "embedding_dimension": { + "name": "embedding_dimension", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 1536 + }, + "chunking_config": { + "name": "chunking_config", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{\"maxSize\": 1024, \"minSize\": 1, \"overlap\": 200}'" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_user_id_idx": { + "name": "kb_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_workspace_id_idx": { + "name": "kb_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_user_workspace_idx": { + "name": "kb_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_deleted_at_idx": { + "name": "kb_deleted_at_idx", + "columns": [ + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_user_id_user_id_fk": { + "name": "knowledge_base_user_id_user_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "knowledge_base_workspace_id_workspace_id_fk": { + "name": "knowledge_base_workspace_id_workspace_id_fk", + "tableFrom": "knowledge_base", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.knowledge_base_tag_definitions": { + "name": "knowledge_base_tag_definitions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "knowledge_base_id": { + "name": "knowledge_base_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tag_slot": { + "name": "tag_slot", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "field_type": { + "name": "field_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "kb_tag_definitions_kb_slot_idx": { + "name": "kb_tag_definitions_kb_slot_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "tag_slot", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_display_name_idx": { + "name": "kb_tag_definitions_kb_display_name_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "display_name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "kb_tag_definitions_kb_id_idx": { + "name": "kb_tag_definitions_kb_id_idx", + "columns": [ + { + "expression": "knowledge_base_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk": { + "name": "knowledge_base_tag_definitions_knowledge_base_id_knowledge_base_id_fk", + "tableFrom": "knowledge_base_tag_definitions", + "tableTo": "knowledge_base", + "columnsFrom": ["knowledge_base_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.mcp_servers": { + "name": "mcp_servers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "transport": { + "name": "transport", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "headers": { + "name": "headers", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "timeout": { + "name": "timeout", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 30000 + }, + "retries": { + "name": "retries", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 3 + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "last_connected": { + "name": "last_connected", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "connection_status": { + "name": "connection_status", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'disconnected'" + }, + "last_error": { + "name": "last_error", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status_config": { + "name": "status_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "tool_count": { + "name": "tool_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_tools_refresh": { + "name": "last_tools_refresh", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_requests": { + "name": "total_requests", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_used": { + "name": "last_used", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "mcp_servers_workspace_enabled_idx": { + "name": "mcp_servers_workspace_enabled_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "enabled", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "mcp_servers_workspace_deleted_idx": { + "name": "mcp_servers_workspace_deleted_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "deleted_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "mcp_servers_workspace_id_workspace_id_fk": { + "name": "mcp_servers_workspace_id_workspace_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "mcp_servers_created_by_user_id_fk": { + "name": "mcp_servers_created_by_user_id_fk", + "tableFrom": "mcp_servers", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.member": { + "name": "member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "member_user_id_unique": { + "name": "member_user_id_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "member_organization_id_idx": { + "name": "member_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "member_user_id_user_id_fk": { + "name": "member_user_id_user_id_fk", + "tableFrom": "member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "member_organization_id_organization_id_fk": { + "name": "member_organization_id_organization_id_fk", + "tableFrom": "member", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.memory": { + "name": "memory", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "deleted_at": { + "name": "deleted_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "memory_key_idx": { + "name": "memory_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_idx": { + "name": "memory_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "memory_workspace_key_idx": { + "name": "memory_workspace_key_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "memory_workspace_id_workspace_id_fk": { + "name": "memory_workspace_id_workspace_id_fk", + "tableFrom": "memory", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.organization": { + "name": "organization", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "slug": { + "name": "slug", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "org_usage_limit": { + "name": "org_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "departed_member_usage": { + "name": "departed_member_usage", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.paused_executions": { + "name": "paused_executions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_snapshot": { + "name": "execution_snapshot", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "pause_points": { + "name": "pause_points", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "total_pause_count": { + "name": "total_pause_count", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "resumed_count": { + "name": "resumed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'paused'" + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'::jsonb" + }, + "paused_at": { + "name": "paused_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "paused_executions_workflow_id_idx": { + "name": "paused_executions_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_status_idx": { + "name": "paused_executions_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "paused_executions_execution_id_unique": { + "name": "paused_executions_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "paused_executions_workflow_id_workflow_id_fk": { + "name": "paused_executions_workflow_id_workflow_id_fk", + "tableFrom": "paused_executions", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group": { + "name": "permission_group", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_organization_id_idx": { + "name": "permission_group_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_created_by_idx": { + "name": "permission_group_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_org_name_unique": { + "name": "permission_group_org_name_unique", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "name", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_organization_id_organization_id_fk": { + "name": "permission_group_organization_id_organization_id_fk", + "tableFrom": "permission_group", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_created_by_user_id_fk": { + "name": "permission_group_created_by_user_id_fk", + "tableFrom": "permission_group", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permission_group_member": { + "name": "permission_group_member", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "permission_group_id": { + "name": "permission_group_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "assigned_by": { + "name": "assigned_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "assigned_at": { + "name": "assigned_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permission_group_member_group_id_idx": { + "name": "permission_group_member_group_id_idx", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_user_id_idx": { + "name": "permission_group_member_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permission_group_member_unique": { + "name": "permission_group_member_unique", + "columns": [ + { + "expression": "permission_group_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permission_group_member_permission_group_id_permission_group_id_fk": { + "name": "permission_group_member_permission_group_id_permission_group_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "permission_group", + "columnsFrom": ["permission_group_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_user_id_user_id_fk": { + "name": "permission_group_member_user_id_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "permission_group_member_assigned_by_user_id_fk": { + "name": "permission_group_member_assigned_by_user_id_fk", + "tableFrom": "permission_group_member", + "tableTo": "user", + "columnsFrom": ["assigned_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.permissions": { + "name": "permissions", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_type": { + "name": "entity_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "entity_id": { + "name": "entity_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permission_type": { + "name": "permission_type", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "permissions_user_id_idx": { + "name": "permissions_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_entity_idx": { + "name": "permissions_entity_idx", + "columns": [ + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_type_idx": { + "name": "permissions_user_entity_type_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_permission_idx": { + "name": "permissions_user_entity_permission_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "permission_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_user_entity_idx": { + "name": "permissions_user_entity_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "permissions_unique_constraint": { + "name": "permissions_unique_constraint", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "entity_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "permissions_user_id_user_id_fk": { + "name": "permissions_user_id_user_id_fk", + "tableFrom": "permissions", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.rate_limit_bucket": { + "name": "rate_limit_bucket", + "schema": "", + "columns": { + "key": { + "name": "key", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "tokens": { + "name": "tokens", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "last_refill_at": { + "name": "last_refill_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.resume_queue": { + "name": "resume_queue", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "paused_execution_id": { + "name": "paused_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_execution_id": { + "name": "parent_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "new_execution_id": { + "name": "new_execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "context_id": { + "name": "context_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "resume_input": { + "name": "resume_input", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "queued_at": { + "name": "queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "claimed_at": { + "name": "claimed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "completed_at": { + "name": "completed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "failure_reason": { + "name": "failure_reason", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "resume_queue_parent_status_idx": { + "name": "resume_queue_parent_status_idx", + "columns": [ + { + "expression": "parent_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "queued_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "resume_queue_new_execution_idx": { + "name": "resume_queue_new_execution_idx", + "columns": [ + { + "expression": "new_execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "resume_queue_paused_execution_id_paused_executions_id_fk": { + "name": "resume_queue_paused_execution_id_paused_executions_id_fk", + "tableFrom": "resume_queue", + "tableTo": "paused_executions", + "columnsFrom": ["paused_execution_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ip_address": { + "name": "ip_address", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_agent": { + "name": "user_agent", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "active_organization_id": { + "name": "active_organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "session_user_id_idx": { + "name": "session_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "session_token_idx": { + "name": "session_token_idx", + "columns": [ + { + "expression": "token", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "session_user_id_user_id_fk": { + "name": "session_user_id_user_id_fk", + "tableFrom": "session", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "session_active_organization_id_organization_id_fk": { + "name": "session_active_organization_id_organization_id_fk", + "tableFrom": "session", + "tableTo": "organization", + "columnsFrom": ["active_organization_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "session_token_unique": { + "name": "session_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.settings": { + "name": "settings", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "theme": { + "name": "theme", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'system'" + }, + "auto_connect": { + "name": "auto_connect", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "telemetry_enabled": { + "name": "telemetry_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "email_preferences": { + "name": "email_preferences", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "billing_usage_notifications_enabled": { + "name": "billing_usage_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "show_training_controls": { + "name": "show_training_controls", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "super_user_mode_enabled": { + "name": "super_user_mode_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "error_notifications_enabled": { + "name": "error_notifications_enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "snap_to_grid_size": { + "name": "snap_to_grid_size", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "copilot_enabled_models": { + "name": "copilot_enabled_models", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "copilot_auto_allowed_tools": { + "name": "copilot_auto_allowed_tools", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "settings_user_id_user_id_fk": { + "name": "settings_user_id_user_id_fk", + "tableFrom": "settings", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "settings_user_id_unique": { + "name": "settings_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.sso_provider": { + "name": "sso_provider", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "issuer": { + "name": "issuer", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "domain": { + "name": "domain", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "oidc_config": { + "name": "oidc_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "saml_config": { + "name": "saml_config", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "organization_id": { + "name": "organization_id", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "sso_provider_provider_id_idx": { + "name": "sso_provider_provider_id_idx", + "columns": [ + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_domain_idx": { + "name": "sso_provider_domain_idx", + "columns": [ + { + "expression": "domain", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_user_id_idx": { + "name": "sso_provider_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "sso_provider_organization_id_idx": { + "name": "sso_provider_organization_id_idx", + "columns": [ + { + "expression": "organization_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "sso_provider_user_id_user_id_fk": { + "name": "sso_provider_user_id_user_id_fk", + "tableFrom": "sso_provider", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "sso_provider_organization_id_organization_id_fk": { + "name": "sso_provider_organization_id_organization_id_fk", + "tableFrom": "sso_provider", + "tableTo": "organization", + "columnsFrom": ["organization_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.subscription": { + "name": "subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "plan": { + "name": "plan", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "stripe_subscription_id": { + "name": "stripe_subscription_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "period_start": { + "name": "period_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "period_end": { + "name": "period_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "cancel_at_period_end": { + "name": "cancel_at_period_end", + "type": "boolean", + "primaryKey": false, + "notNull": false + }, + "seats": { + "name": "seats", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "trial_start": { + "name": "trial_start", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trial_end": { + "name": "trial_end", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "metadata": { + "name": "metadata", + "type": "json", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "subscription_reference_status_idx": { + "name": "subscription_reference_status_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": { + "check_enterprise_metadata": { + "name": "check_enterprise_metadata", + "value": "plan != 'enterprise' OR metadata IS NOT NULL" + } + }, + "isRLSEnabled": false + }, + "public.template_creators": { + "name": "template_creators", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "reference_type": { + "name": "reference_type", + "type": "template_creator_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "reference_id": { + "name": "reference_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "profile_image_url": { + "name": "profile_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "verified": { + "name": "verified", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_creators_reference_idx": { + "name": "template_creators_reference_idx", + "columns": [ + { + "expression": "reference_type", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_reference_id_idx": { + "name": "template_creators_reference_id_idx", + "columns": [ + { + "expression": "reference_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_creators_created_by_idx": { + "name": "template_creators_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_creators_created_by_user_id_fk": { + "name": "template_creators_created_by_user_id_fk", + "tableFrom": "template_creators", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.template_stars": { + "name": "template_stars", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "template_id": { + "name": "template_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "starred_at": { + "name": "starred_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "template_stars_user_id_idx": { + "name": "template_stars_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_id_idx": { + "name": "template_stars_template_id_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_idx": { + "name": "template_stars_user_template_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_user_idx": { + "name": "template_stars_template_user_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_starred_at_idx": { + "name": "template_stars_starred_at_idx", + "columns": [ + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_template_starred_at_idx": { + "name": "template_stars_template_starred_at_idx", + "columns": [ + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "starred_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "template_stars_user_template_unique": { + "name": "template_stars_user_template_unique", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "template_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "template_stars_user_id_user_id_fk": { + "name": "template_stars_user_id_user_id_fk", + "tableFrom": "template_stars", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "template_stars_template_id_templates_id_fk": { + "name": "template_stars_template_id_templates_id_fk", + "tableFrom": "template_stars", + "tableTo": "templates", + "columnsFrom": ["template_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.templates": { + "name": "templates", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "details": { + "name": "details", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "creator_id": { + "name": "creator_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "views": { + "name": "views", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "stars": { + "name": "stars", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "template_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "tags": { + "name": "tags", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "required_credentials": { + "name": "required_credentials", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'[]'" + }, + "state": { + "name": "state", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "og_image_url": { + "name": "og_image_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "templates_status_idx": { + "name": "templates_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_creator_id_idx": { + "name": "templates_creator_id_idx", + "columns": [ + { + "expression": "creator_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_views_idx": { + "name": "templates_views_idx", + "columns": [ + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_stars_idx": { + "name": "templates_stars_idx", + "columns": [ + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_views_idx": { + "name": "templates_status_views_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "views", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_status_stars_idx": { + "name": "templates_status_stars_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "stars", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_created_at_idx": { + "name": "templates_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "templates_updated_at_idx": { + "name": "templates_updated_at_idx", + "columns": [ + { + "expression": "updated_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "templates_workflow_id_workflow_id_fk": { + "name": "templates_workflow_id_workflow_id_fk", + "tableFrom": "templates", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "templates_creator_id_template_creators_id_fk": { + "name": "templates_creator_id_template_creators_id_fk", + "tableFrom": "templates", + "tableTo": "template_creators", + "columnsFrom": ["creator_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.usage_log": { + "name": "usage_log", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "category": { + "name": "category", + "type": "usage_log_category", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "source": { + "name": "source", + "type": "usage_log_source", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "metadata": { + "name": "metadata", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "cost": { + "name": "cost", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "usage_log_user_created_at_idx": { + "name": "usage_log_user_created_at_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_source_idx": { + "name": "usage_log_source_idx", + "columns": [ + { + "expression": "source", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workspace_id_idx": { + "name": "usage_log_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "usage_log_workflow_id_idx": { + "name": "usage_log_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "usage_log_user_id_user_id_fk": { + "name": "usage_log_user_id_user_id_fk", + "tableFrom": "usage_log", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "usage_log_workspace_id_workspace_id_fk": { + "name": "usage_log_workspace_id_workspace_id_fk", + "tableFrom": "usage_log", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + }, + "usage_log_workflow_id_workflow_id_fk": { + "name": "usage_log_workflow_id_workflow_id_fk", + "tableFrom": "usage_log", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user": { + "name": "user", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email_verified": { + "name": "email_verified", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "image": { + "name": "image", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "stripe_customer_id": { + "name": "stripe_customer_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "is_super_user": { + "name": "is_super_user", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_email_unique": { + "name": "user_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.user_stats": { + "name": "user_stats", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "total_manual_executions": { + "name": "total_manual_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_api_calls": { + "name": "total_api_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_webhook_triggers": { + "name": "total_webhook_triggers", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_scheduled_executions": { + "name": "total_scheduled_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_chat_executions": { + "name": "total_chat_executions", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_tokens_used": { + "name": "total_tokens_used", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_cost": { + "name": "total_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_usage_limit": { + "name": "current_usage_limit", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'20'" + }, + "usage_limit_updated_at": { + "name": "usage_limit_updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false, + "default": "now()" + }, + "current_period_cost": { + "name": "current_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_cost": { + "name": "last_period_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "billed_overage_this_period": { + "name": "billed_overage_this_period", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "pro_period_cost_snapshot": { + "name": "pro_period_cost_snapshot", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "credit_balance": { + "name": "credit_balance", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "total_copilot_cost": { + "name": "total_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "current_period_copilot_cost": { + "name": "current_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "last_period_copilot_cost": { + "name": "last_period_copilot_cost", + "type": "numeric", + "primaryKey": false, + "notNull": false, + "default": "'0'" + }, + "total_copilot_tokens": { + "name": "total_copilot_tokens", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "total_copilot_calls": { + "name": "total_copilot_calls", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "storage_used_bytes": { + "name": "storage_used_bytes", + "type": "bigint", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_active": { + "name": "last_active", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "billing_blocked": { + "name": "billing_blocked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "billing_blocked_reason": { + "name": "billing_blocked_reason", + "type": "billing_blocked_reason", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "user_stats_user_id_user_id_fk": { + "name": "user_stats_user_id_user_id_fk", + "tableFrom": "user_stats", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "user_stats_user_id_unique": { + "name": "user_stats_user_id_unique", + "nullsNotDistinct": false, + "columns": ["user_id"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.verification": { + "name": "verification", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "identifier": { + "name": "identifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "value": { + "name": "value", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "verification_identifier_idx": { + "name": "verification_identifier_idx", + "columns": [ + { + "expression": "identifier", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "verification_expires_at_idx": { + "name": "verification_expires_at_idx", + "columns": [ + { + "expression": "expires_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.waitlist": { + "name": "waitlist", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "waitlist_email_unique": { + "name": "waitlist_email_unique", + "nullsNotDistinct": false, + "columns": ["email"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.webhook": { + "name": "webhook", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "path": { + "name": "path", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider": { + "name": "provider", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "provider_config": { + "name": "provider_config", + "type": "json", + "primaryKey": false, + "notNull": false + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": false, + "default": 0 + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "credential_set_id": { + "name": "credential_set_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "path_idx": { + "name": "path_idx", + "columns": [ + { + "expression": "path", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "idx_webhook_on_workflow_id_block_id": { + "name": "idx_webhook_on_workflow_id_block_id", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "webhook_credential_set_id_idx": { + "name": "webhook_credential_set_id_idx", + "columns": [ + { + "expression": "credential_set_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "webhook_workflow_id_workflow_id_fk": { + "name": "webhook_workflow_id_workflow_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_block_id_workflow_blocks_id_fk": { + "name": "webhook_block_id_workflow_blocks_id_fk", + "tableFrom": "webhook", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "webhook_credential_set_id_credential_set_id_fk": { + "name": "webhook_credential_set_id_credential_set_id_fk", + "tableFrom": "webhook", + "tableTo": "credential_set", + "columnsFrom": ["credential_set_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow": { + "name": "workflow", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "folder_id": { + "name": "folder_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'#3972F6'" + }, + "last_synced": { + "name": "last_synced", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "is_deployed": { + "name": "is_deployed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "deployed_at": { + "name": "deployed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "run_count": { + "name": "run_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_run_at": { + "name": "last_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + } + }, + "indexes": { + "workflow_user_id_idx": { + "name": "workflow_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_workspace_id_idx": { + "name": "workflow_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_user_workspace_idx": { + "name": "workflow_user_workspace_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_user_id_user_id_fk": { + "name": "workflow_user_id_user_id_fk", + "tableFrom": "workflow", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_workspace_id_workspace_id_fk": { + "name": "workflow_workspace_id_workspace_id_fk", + "tableFrom": "workflow", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_id_workflow_folder_id_fk": { + "name": "workflow_folder_id_workflow_folder_id_fk", + "tableFrom": "workflow", + "tableTo": "workflow_folder", + "columnsFrom": ["folder_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_blocks": { + "name": "workflow_blocks", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "position_x": { + "name": "position_x", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "position_y": { + "name": "position_y", + "type": "numeric", + "primaryKey": false, + "notNull": true + }, + "enabled": { + "name": "enabled", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "horizontal_handles": { + "name": "horizontal_handles", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "is_wide": { + "name": "is_wide", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "advanced_mode": { + "name": "advanced_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "trigger_mode": { + "name": "trigger_mode", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "height": { + "name": "height", + "type": "numeric", + "primaryKey": false, + "notNull": true, + "default": "'0'" + }, + "sub_blocks": { + "name": "sub_blocks", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "outputs": { + "name": "outputs", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "data": { + "name": "data", + "type": "jsonb", + "primaryKey": false, + "notNull": false, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_blocks_workflow_id_idx": { + "name": "workflow_blocks_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_blocks_type_idx": { + "name": "workflow_blocks_type_idx", + "columns": [ + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_blocks_workflow_id_workflow_id_fk": { + "name": "workflow_blocks_workflow_id_workflow_id_fk", + "tableFrom": "workflow_blocks", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_checkpoints": { + "name": "workflow_checkpoints", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "gen_random_uuid()" + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "chat_id": { + "name": "chat_id", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "message_id": { + "name": "message_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "workflow_state": { + "name": "workflow_state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_checkpoints_user_id_idx": { + "name": "workflow_checkpoints_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_id_idx": { + "name": "workflow_checkpoints_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_id_idx": { + "name": "workflow_checkpoints_chat_id_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_message_id_idx": { + "name": "workflow_checkpoints_message_id_idx", + "columns": [ + { + "expression": "message_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_user_workflow_idx": { + "name": "workflow_checkpoints_user_workflow_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_workflow_chat_idx": { + "name": "workflow_checkpoints_workflow_chat_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_created_at_idx": { + "name": "workflow_checkpoints_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_checkpoints_chat_created_at_idx": { + "name": "workflow_checkpoints_chat_created_at_idx", + "columns": [ + { + "expression": "chat_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_checkpoints_user_id_user_id_fk": { + "name": "workflow_checkpoints_user_id_user_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_workflow_id_workflow_id_fk": { + "name": "workflow_checkpoints_workflow_id_workflow_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_checkpoints_chat_id_copilot_chats_id_fk": { + "name": "workflow_checkpoints_chat_id_copilot_chats_id_fk", + "tableFrom": "workflow_checkpoints", + "tableTo": "copilot_chats", + "columnsFrom": ["chat_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_deployment_version": { + "name": "workflow_deployment_version", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "state": { + "name": "state", + "type": "json", + "primaryKey": false, + "notNull": true + }, + "is_active": { + "name": "is_active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "workflow_deployment_version_workflow_version_unique": { + "name": "workflow_deployment_version_workflow_version_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "version", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_workflow_active_idx": { + "name": "workflow_deployment_version_workflow_active_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "is_active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_deployment_version_created_at_idx": { + "name": "workflow_deployment_version_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_deployment_version_workflow_id_workflow_id_fk": { + "name": "workflow_deployment_version_workflow_id_workflow_id_fk", + "tableFrom": "workflow_deployment_version", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_edges": { + "name": "workflow_edges", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_block_id": { + "name": "source_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "target_block_id": { + "name": "target_block_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "source_handle": { + "name": "source_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "target_handle": { + "name": "target_handle", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_edges_workflow_id_idx": { + "name": "workflow_edges_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_source_idx": { + "name": "workflow_edges_workflow_source_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "source_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_edges_workflow_target_idx": { + "name": "workflow_edges_workflow_target_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "target_block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_edges_workflow_id_workflow_id_fk": { + "name": "workflow_edges_workflow_id_workflow_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_source_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_source_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["source_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_edges_target_block_id_workflow_blocks_id_fk": { + "name": "workflow_edges_target_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_edges", + "tableTo": "workflow_blocks", + "columnsFrom": ["target_block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_logs": { + "name": "workflow_execution_logs", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_snapshot_id": { + "name": "state_snapshot_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "deployment_version_id": { + "name": "deployment_version_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "level": { + "name": "level", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'running'" + }, + "trigger": { + "name": "trigger", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "started_at": { + "name": "started_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "ended_at": { + "name": "ended_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "total_duration_ms": { + "name": "total_duration_ms", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "execution_data": { + "name": "execution_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "cost": { + "name": "cost", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "files": { + "name": "files", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_execution_logs_workflow_id_idx": { + "name": "workflow_execution_logs_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_state_snapshot_id_idx": { + "name": "workflow_execution_logs_state_snapshot_id_idx", + "columns": [ + { + "expression": "state_snapshot_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_deployment_version_id_idx": { + "name": "workflow_execution_logs_deployment_version_id_idx", + "columns": [ + { + "expression": "deployment_version_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_trigger_idx": { + "name": "workflow_execution_logs_trigger_idx", + "columns": [ + { + "expression": "trigger", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_level_idx": { + "name": "workflow_execution_logs_level_idx", + "columns": [ + { + "expression": "level", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_started_at_idx": { + "name": "workflow_execution_logs_started_at_idx", + "columns": [ + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_execution_id_unique": { + "name": "workflow_execution_logs_execution_id_unique", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workflow_started_at_idx": { + "name": "workflow_execution_logs_workflow_started_at_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_execution_logs_workspace_started_at_idx": { + "name": "workflow_execution_logs_workspace_started_at_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "started_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_logs_workflow_id_workflow_id_fk": { + "name": "workflow_execution_logs_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_workspace_id_workspace_id_fk": { + "name": "workflow_execution_logs_workspace_id_workspace_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk": { + "name": "workflow_execution_logs_state_snapshot_id_workflow_execution_snapshots_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_execution_snapshots", + "columnsFrom": ["state_snapshot_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + }, + "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk": { + "name": "workflow_execution_logs_deployment_version_id_workflow_deployment_version_id_fk", + "tableFrom": "workflow_execution_logs", + "tableTo": "workflow_deployment_version", + "columnsFrom": ["deployment_version_id"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_execution_snapshots": { + "name": "workflow_execution_snapshots", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_hash": { + "name": "state_hash", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "state_data": { + "name": "state_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_snapshots_workflow_id_idx": { + "name": "workflow_snapshots_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_hash_idx": { + "name": "workflow_snapshots_hash_idx", + "columns": [ + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_workflow_hash_idx": { + "name": "workflow_snapshots_workflow_hash_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "state_hash", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_snapshots_created_at_idx": { + "name": "workflow_snapshots_created_at_idx", + "columns": [ + { + "expression": "created_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_execution_snapshots_workflow_id_workflow_id_fk": { + "name": "workflow_execution_snapshots_workflow_id_workflow_id_fk", + "tableFrom": "workflow_execution_snapshots", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_folder": { + "name": "workflow_folder", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "parent_id": { + "name": "parent_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "color": { + "name": "color", + "type": "text", + "primaryKey": false, + "notNull": false, + "default": "'#6B7280'" + }, + "is_expanded": { + "name": "is_expanded", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "sort_order": { + "name": "sort_order", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_folder_user_idx": { + "name": "workflow_folder_user_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_workspace_parent_idx": { + "name": "workflow_folder_workspace_parent_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_folder_parent_sort_idx": { + "name": "workflow_folder_parent_sort_idx", + "columns": [ + { + "expression": "parent_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "sort_order", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_folder_user_id_user_id_fk": { + "name": "workflow_folder_user_id_user_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_folder_workspace_id_workspace_id_fk": { + "name": "workflow_folder_workspace_id_workspace_id_fk", + "tableFrom": "workflow_folder", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_server": { + "name": "workflow_mcp_server", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_server_workspace_id_idx": { + "name": "workflow_mcp_server_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_server_created_by_idx": { + "name": "workflow_mcp_server_created_by_idx", + "columns": [ + { + "expression": "created_by", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_server_workspace_id_workspace_id_fk": { + "name": "workflow_mcp_server_workspace_id_workspace_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_server_created_by_user_id_fk": { + "name": "workflow_mcp_server_created_by_user_id_fk", + "tableFrom": "workflow_mcp_server", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_mcp_tool": { + "name": "workflow_mcp_tool", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_name": { + "name": "tool_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "tool_description": { + "name": "tool_description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "parameter_schema": { + "name": "parameter_schema", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_mcp_tool_server_id_idx": { + "name": "workflow_mcp_tool_server_id_idx", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_workflow_id_idx": { + "name": "workflow_mcp_tool_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_mcp_tool_server_workflow_unique": { + "name": "workflow_mcp_tool_server_workflow_unique", + "columns": [ + { + "expression": "server_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk": { + "name": "workflow_mcp_tool_server_id_workflow_mcp_server_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow_mcp_server", + "columnsFrom": ["server_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_mcp_tool_workflow_id_workflow_id_fk": { + "name": "workflow_mcp_tool_workflow_id_workflow_id_fk", + "tableFrom": "workflow_mcp_tool", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_schedule": { + "name": "workflow_schedule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "block_id": { + "name": "block_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "cron_expression": { + "name": "cron_expression", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "next_run_at": { + "name": "next_run_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_ran_at": { + "name": "last_ran_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "last_queued_at": { + "name": "last_queued_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "trigger_type": { + "name": "trigger_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "timezone": { + "name": "timezone", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'UTC'" + }, + "failed_count": { + "name": "failed_count", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "status": { + "name": "status", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'active'" + }, + "last_failed_at": { + "name": "last_failed_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_schedule_workflow_block_unique": { + "name": "workflow_schedule_workflow_block_unique", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "block_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_schedule_workflow_id_workflow_id_fk": { + "name": "workflow_schedule_workflow_id_workflow_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workflow_schedule_block_id_workflow_blocks_id_fk": { + "name": "workflow_schedule_block_id_workflow_blocks_id_fk", + "tableFrom": "workflow_schedule", + "tableTo": "workflow_blocks", + "columnsFrom": ["block_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workflow_subflows": { + "name": "workflow_subflows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "config": { + "name": "config", + "type": "jsonb", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workflow_subflows_workflow_id_idx": { + "name": "workflow_subflows_workflow_id_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workflow_subflows_workflow_type_idx": { + "name": "workflow_subflows_workflow_type_idx", + "columns": [ + { + "expression": "workflow_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workflow_subflows_workflow_id_workflow_id_fk": { + "name": "workflow_subflows_workflow_id_workflow_id_fk", + "tableFrom": "workflow_subflows", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace": { + "name": "workspace", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "owner_id": { + "name": "owner_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "billed_account_user_id": { + "name": "billed_account_user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "allow_personal_api_keys": { + "name": "allow_personal_api_keys", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_owner_id_user_id_fk": { + "name": "workspace_owner_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["owner_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_billed_account_user_id_user_id_fk": { + "name": "workspace_billed_account_user_id_user_id_fk", + "tableFrom": "workspace", + "tableTo": "user", + "columnsFrom": ["billed_account_user_id"], + "columnsTo": ["id"], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_byok_keys": { + "name": "workspace_byok_keys", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "provider_id": { + "name": "provider_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "encrypted_api_key": { + "name": "encrypted_api_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_byok_provider_unique": { + "name": "workspace_byok_provider_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "provider_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_byok_workspace_idx": { + "name": "workspace_byok_workspace_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_byok_keys_workspace_id_workspace_id_fk": { + "name": "workspace_byok_keys_workspace_id_workspace_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_byok_keys_created_by_user_id_fk": { + "name": "workspace_byok_keys_created_by_user_id_fk", + "tableFrom": "workspace_byok_keys", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "set null", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_environment": { + "name": "workspace_environment", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "variables": { + "name": "variables", + "type": "json", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_environment_workspace_unique": { + "name": "workspace_environment_workspace_unique", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_environment_workspace_id_workspace_id_fk": { + "name": "workspace_environment_workspace_id_workspace_id_fk", + "tableFrom": "workspace_environment", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_file": { + "name": "workspace_file", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_by": { + "name": "uploaded_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_file_workspace_id_idx": { + "name": "workspace_file_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_file_key_idx": { + "name": "workspace_file_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_file_workspace_id_workspace_id_fk": { + "name": "workspace_file_workspace_id_workspace_id_fk", + "tableFrom": "workspace_file", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_file_uploaded_by_user_id_fk": { + "name": "workspace_file_uploaded_by_user_id_fk", + "tableFrom": "workspace_file", + "tableTo": "user", + "columnsFrom": ["uploaded_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_file_key_unique": { + "name": "workspace_file_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_files": { + "name": "workspace_files", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "key": { + "name": "key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "context": { + "name": "context", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "original_name": { + "name": "original_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": true + }, + "uploaded_at": { + "name": "uploaded_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_files_key_idx": { + "name": "workspace_files_key_idx", + "columns": [ + { + "expression": "key", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_user_id_idx": { + "name": "workspace_files_user_id_idx", + "columns": [ + { + "expression": "user_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_workspace_id_idx": { + "name": "workspace_files_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_files_context_idx": { + "name": "workspace_files_context_idx", + "columns": [ + { + "expression": "context", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_files_user_id_user_id_fk": { + "name": "workspace_files_user_id_user_id_fk", + "tableFrom": "workspace_files", + "tableTo": "user", + "columnsFrom": ["user_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_files_workspace_id_workspace_id_fk": { + "name": "workspace_files_workspace_id_workspace_id_fk", + "tableFrom": "workspace_files", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_files_key_unique": { + "name": "workspace_files_key_unique", + "nullsNotDistinct": false, + "columns": ["key"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_invitation": { + "name": "workspace_invitation", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "inviter_id": { + "name": "inviter_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "role": { + "name": "role", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'member'" + }, + "status": { + "name": "status", + "type": "workspace_invitation_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "token": { + "name": "token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "permissions": { + "name": "permissions", + "type": "permission_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'admin'" + }, + "org_invitation_id": { + "name": "org_invitation_id", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "workspace_invitation_workspace_id_workspace_id_fk": { + "name": "workspace_invitation_workspace_id_workspace_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_invitation_inviter_id_user_id_fk": { + "name": "workspace_invitation_inviter_id_user_id_fk", + "tableFrom": "workspace_invitation", + "tableTo": "user", + "columnsFrom": ["inviter_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": { + "workspace_invitation_token_unique": { + "name": "workspace_invitation_token_unique", + "nullsNotDistinct": false, + "columns": ["token"] + } + }, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_delivery": { + "name": "workspace_notification_delivery", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "subscription_id": { + "name": "subscription_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "workflow_id": { + "name": "workflow_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "execution_id": { + "name": "execution_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "status": { + "name": "status", + "type": "notification_delivery_status", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'pending'" + }, + "attempts": { + "name": "attempts", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + }, + "last_attempt_at": { + "name": "last_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "next_attempt_at": { + "name": "next_attempt_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "response_status": { + "name": "response_status", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "response_body": { + "name": "response_body", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "error_message": { + "name": "error_message", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_delivery_subscription_id_idx": { + "name": "workspace_notification_delivery_subscription_id_idx", + "columns": [ + { + "expression": "subscription_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_execution_id_idx": { + "name": "workspace_notification_delivery_execution_id_idx", + "columns": [ + { + "expression": "execution_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_status_idx": { + "name": "workspace_notification_delivery_status_idx", + "columns": [ + { + "expression": "status", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_delivery_next_attempt_idx": { + "name": "workspace_notification_delivery_next_attempt_idx", + "columns": [ + { + "expression": "next_attempt_at", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk": { + "name": "workspace_notification_delivery_subscription_id_workspace_notification_subscription_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workspace_notification_subscription", + "columnsFrom": ["subscription_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_delivery_workflow_id_workflow_id_fk": { + "name": "workspace_notification_delivery_workflow_id_workflow_id_fk", + "tableFrom": "workspace_notification_delivery", + "tableTo": "workflow", + "columnsFrom": ["workflow_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.workspace_notification_subscription": { + "name": "workspace_notification_subscription", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "workspace_id": { + "name": "workspace_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "notification_type": { + "name": "notification_type", + "type": "notification_type", + "typeSchema": "public", + "primaryKey": false, + "notNull": true + }, + "workflow_ids": { + "name": "workflow_ids", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'::text[]" + }, + "all_workflows": { + "name": "all_workflows", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "level_filter": { + "name": "level_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['info', 'error']::text[]" + }, + "trigger_filter": { + "name": "trigger_filter", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "ARRAY['api', 'webhook', 'schedule', 'manual', 'chat']::text[]" + }, + "include_final_output": { + "name": "include_final_output", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_trace_spans": { + "name": "include_trace_spans", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_rate_limits": { + "name": "include_rate_limits", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "include_usage_data": { + "name": "include_usage_data", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "webhook_config": { + "name": "webhook_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "email_recipients": { + "name": "email_recipients", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "slack_config": { + "name": "slack_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "alert_config": { + "name": "alert_config", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "last_alert_at": { + "name": "last_alert_at", + "type": "timestamp", + "primaryKey": false, + "notNull": false + }, + "active": { + "name": "active", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": true + }, + "created_by": { + "name": "created_by", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": { + "workspace_notification_workspace_id_idx": { + "name": "workspace_notification_workspace_id_idx", + "columns": [ + { + "expression": "workspace_id", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_active_idx": { + "name": "workspace_notification_active_idx", + "columns": [ + { + "expression": "active", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + }, + "workspace_notification_type_idx": { + "name": "workspace_notification_type_idx", + "columns": [ + { + "expression": "notification_type", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": false, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "workspace_notification_subscription_workspace_id_workspace_id_fk": { + "name": "workspace_notification_subscription_workspace_id_workspace_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "workspace", + "columnsFrom": ["workspace_id"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + }, + "workspace_notification_subscription_created_by_user_id_fk": { + "name": "workspace_notification_subscription_created_by_user_id_fk", + "tableFrom": "workspace_notification_subscription", + "tableTo": "user", + "columnsFrom": ["created_by"], + "columnsTo": ["id"], + "onDelete": "cascade", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.billing_blocked_reason": { + "name": "billing_blocked_reason", + "schema": "public", + "values": ["payment_failed", "dispute"] + }, + "public.credential_set_invitation_status": { + "name": "credential_set_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "expired", "cancelled"] + }, + "public.credential_set_member_status": { + "name": "credential_set_member_status", + "schema": "public", + "values": ["active", "pending", "revoked"] + }, + "public.notification_delivery_status": { + "name": "notification_delivery_status", + "schema": "public", + "values": ["pending", "in_progress", "success", "failed"] + }, + "public.notification_type": { + "name": "notification_type", + "schema": "public", + "values": ["webhook", "email", "slack"] + }, + "public.permission_type": { + "name": "permission_type", + "schema": "public", + "values": ["admin", "write", "read"] + }, + "public.template_creator_type": { + "name": "template_creator_type", + "schema": "public", + "values": ["user", "organization"] + }, + "public.template_status": { + "name": "template_status", + "schema": "public", + "values": ["pending", "approved", "rejected"] + }, + "public.usage_log_category": { + "name": "usage_log_category", + "schema": "public", + "values": ["model", "fixed"] + }, + "public.usage_log_source": { + "name": "usage_log_source", + "schema": "public", + "values": ["workflow", "wand", "copilot"] + }, + "public.workspace_invitation_status": { + "name": "workspace_invitation_status", + "schema": "public", + "values": ["pending", "accepted", "rejected", "cancelled"] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index 1094a146ba..b8fe5b2b22 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -953,6 +953,13 @@ "when": 1767905804764, "tag": "0136_pretty_jack_flag", "breakpoints": true + }, + { + "idx": 137, + "version": "7", + "when": 1767922492454, + "tag": "0137_absurd_sumo", + "breakpoints": true } ] } From aba68ba446d05dca302638d0ec3ba08f0fb6594a Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 17:38:52 -0800 Subject: [PATCH 10/14] fix --- apps/sim/executor/handlers/agent/agent-handler.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/apps/sim/executor/handlers/agent/agent-handler.test.ts b/apps/sim/executor/handlers/agent/agent-handler.test.ts index d0e2595333..e8ff34115e 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.test.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.test.ts @@ -18,6 +18,7 @@ vi.mock('@/lib/core/config/feature-flags', () => ({ getCostMultiplier: vi.fn().mockReturnValue(1), isEmailVerificationEnabled: false, isBillingEnabled: false, + isOrganizationsEnabled: false, })) vi.mock('@/providers/utils', () => ({ From 2516396191d98ba619e0aca2b1a6125857613254 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 17:57:51 -0800 Subject: [PATCH 11/14] address greptile comments --- .../[id]/members/bulk/route.ts | 4 +- .../permission-groups/[id]/members/route.ts | 70 ++++++++++--------- .../components/tool-input/tool-input.tsx | 1 + apps/sim/executor/execution/block-executor.ts | 2 +- .../executor/handlers/agent/agent-handler.ts | 8 +-- .../handlers/evaluator/evaluator-handler.ts | 2 +- .../handlers/router/router-handler.ts | 4 +- apps/sim/executor/types.ts | 4 ++ apps/sim/executor/utils/permission-check.ts | 47 ++++++++++--- 9 files changed, 92 insertions(+), 50 deletions(-) diff --git a/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts b/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts index 460a37c200..dc4a94528e 100644 --- a/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, inArray, notInArray } from 'drizzle-orm' +import { and, eq, inArray, ne } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' @@ -123,7 +123,7 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: and( eq(permissionGroup.organizationId, result.group.organizationId), inArray(permissionGroupMember.userId, usersToAdd), - notInArray(permissionGroupMember.permissionGroupId, [id]) + ne(permissionGroupMember.permissionGroupId, id) ) ) diff --git a/apps/sim/app/api/permission-groups/[id]/members/route.ts b/apps/sim/app/api/permission-groups/[id]/members/route.ts index 130704f5fa..172472f54b 100644 --- a/apps/sim/app/api/permission-groups/[id]/members/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/members/route.ts @@ -110,42 +110,42 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: ) } - const existingMembership = await db - .select({ - id: permissionGroupMember.id, - permissionGroupId: permissionGroupMember.permissionGroupId, - }) - .from(permissionGroupMember) - .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) - .where( - and( - eq(permissionGroupMember.userId, userId), - eq(permissionGroup.organizationId, result.group.organizationId) - ) - ) - .limit(1) - - if (existingMembership.length > 0) { - if (existingMembership[0].permissionGroupId === id) { - return NextResponse.json( - { error: 'User is already in this permission group' }, - { status: 409 } + const newMember = await db.transaction(async (tx) => { + const existingMembership = await tx + .select({ + id: permissionGroupMember.id, + permissionGroupId: permissionGroupMember.permissionGroupId, + }) + .from(permissionGroupMember) + .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) + .where( + and( + eq(permissionGroupMember.userId, userId), + eq(permissionGroup.organizationId, result.group.organizationId) + ) ) + .limit(1) + + if (existingMembership.length > 0) { + if (existingMembership[0].permissionGroupId === id) { + throw new Error('ALREADY_IN_GROUP') + } + await tx + .delete(permissionGroupMember) + .where(eq(permissionGroupMember.id, existingMembership[0].id)) } - await db - .delete(permissionGroupMember) - .where(eq(permissionGroupMember.id, existingMembership[0].id)) - } - const newMember = { - id: crypto.randomUUID(), - permissionGroupId: id, - userId, - assignedBy: session.user.id, - assignedAt: new Date(), - } + const memberData = { + id: crypto.randomUUID(), + permissionGroupId: id, + userId, + assignedBy: session.user.id, + assignedAt: new Date(), + } - await db.insert(permissionGroupMember).values(newMember) + await tx.insert(permissionGroupMember).values(memberData) + return memberData + }) logger.info('Added member to permission group', { permissionGroupId: id, @@ -158,6 +158,12 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: if (error instanceof z.ZodError) { return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) } + if (error instanceof Error && error.message === 'ALREADY_IN_GROUP') { + return NextResponse.json( + { error: 'User is already in this permission group' }, + { status: 409 } + ) + } logger.error('Error adding member to permission group', error) return NextResponse.json({ error: 'Failed to add member' }, { status: 500 }) } diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx index 8a380eace9..6532c60600 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tool-input/tool-input.tsx @@ -7,6 +7,7 @@ import { useParams } from 'next/navigation' import { Badge, Combobox, + type ComboboxOption, type ComboboxOptionGroup, Popover, PopoverContent, diff --git a/apps/sim/executor/execution/block-executor.ts b/apps/sim/executor/execution/block-executor.ts index 8bb9f76f62..116056d35e 100644 --- a/apps/sim/executor/execution/block-executor.ts +++ b/apps/sim/executor/execution/block-executor.ts @@ -77,7 +77,7 @@ export class BlockExecutor { try { if (!isSentinel && blockType) { - await validateBlockType(ctx.userId, blockType) + await validateBlockType(ctx.userId, blockType, ctx) } resolvedInputs = this.resolver.resolveInputs(ctx, node.id, block.config.params, block) diff --git a/apps/sim/executor/handlers/agent/agent-handler.ts b/apps/sim/executor/handlers/agent/agent-handler.ts index 6640546f4a..b1613aa6eb 100644 --- a/apps/sim/executor/handlers/agent/agent-handler.ts +++ b/apps/sim/executor/handlers/agent/agent-handler.ts @@ -62,7 +62,7 @@ export class AgentBlockHandler implements BlockHandler { const responseFormat = this.parseResponseFormat(filteredInputs.responseFormat) const model = filteredInputs.model || AGENT.DEFAULT_MODEL - await validateModelProvider(ctx.userId, model) + await validateModelProvider(ctx.userId, model, ctx) const providerId = getProviderFromModel(model) const formattedTools = await this.formatTools(ctx, filteredInputs.tools || []) @@ -162,11 +162,11 @@ export class AgentBlockHandler implements BlockHandler { const hasCustomTools = tools.some((t) => t.type === 'custom-tool') if (hasMcpTools) { - await validateMcpToolsAllowed(ctx.userId) + await validateMcpToolsAllowed(ctx.userId, ctx) } if (hasCustomTools) { - await validateCustomToolsAllowed(ctx.userId) + await validateCustomToolsAllowed(ctx.userId, ctx) } } @@ -240,7 +240,7 @@ export class AgentBlockHandler implements BlockHandler { otherTools.map(async (tool) => { try { if (tool.type && tool.type !== 'custom-tool' && tool.type !== 'mcp') { - await validateBlockType(ctx.userId, tool.type) + await validateBlockType(ctx.userId, tool.type, ctx) } if (tool.type === 'custom-tool' && (tool.schema || tool.customToolId)) { return await this.createCustomTool(ctx, tool) diff --git a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts index 29ee24e4f3..9cc03cb395 100644 --- a/apps/sim/executor/handlers/evaluator/evaluator-handler.ts +++ b/apps/sim/executor/handlers/evaluator/evaluator-handler.ts @@ -38,7 +38,7 @@ export class EvaluatorBlockHandler implements BlockHandler { bedrockRegion: inputs.bedrockRegion, } - await validateModelProvider(ctx.userId, evaluatorConfig.model) + await validateModelProvider(ctx.userId, evaluatorConfig.model, ctx) const providerId = getProviderFromModel(evaluatorConfig.model) diff --git a/apps/sim/executor/handlers/router/router-handler.ts b/apps/sim/executor/handlers/router/router-handler.ts index cdd2236f9e..3f3e45d1b4 100644 --- a/apps/sim/executor/handlers/router/router-handler.ts +++ b/apps/sim/executor/handlers/router/router-handler.ts @@ -74,7 +74,7 @@ export class RouterBlockHandler implements BlockHandler { bedrockRegion: inputs.bedrockRegion, } - await validateModelProvider(ctx.userId, routerConfig.model) + await validateModelProvider(ctx.userId, routerConfig.model, ctx) const providerId = getProviderFromModel(routerConfig.model) @@ -214,7 +214,7 @@ export class RouterBlockHandler implements BlockHandler { bedrockRegion: inputs.bedrockRegion, } - await validateModelProvider(ctx.userId, routerConfig.model) + await validateModelProvider(ctx.userId, routerConfig.model, ctx) const providerId = getProviderFromModel(routerConfig.model) diff --git a/apps/sim/executor/types.ts b/apps/sim/executor/types.ts index 9c266e8b40..c0d96a81e5 100644 --- a/apps/sim/executor/types.ts +++ b/apps/sim/executor/types.ts @@ -1,4 +1,5 @@ import type { TraceSpan } from '@/lib/logs/types' +import type { PermissionGroupConfig } from '@/lib/permission-groups/types' import type { BlockOutput } from '@/blocks/types' import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types' @@ -152,6 +153,9 @@ export interface ExecutionContext { userId?: string isDeployedContext?: boolean + permissionConfig?: PermissionGroupConfig | null + permissionConfigLoaded?: boolean + blockStates: ReadonlyMap executedBlocks: ReadonlySet diff --git a/apps/sim/executor/utils/permission-check.ts b/apps/sim/executor/utils/permission-check.ts index 232b44940c..5e24df54ce 100644 --- a/apps/sim/executor/utils/permission-check.ts +++ b/apps/sim/executor/utils/permission-check.ts @@ -7,6 +7,7 @@ import { type PermissionGroupConfig, parsePermissionGroupConfig, } from '@/lib/permission-groups/types' +import type { ExecutionContext } from '@/executor/types' import { getProviderFromModel } from '@/providers/utils' const logger = createLogger('PermissionCheck') @@ -78,15 +79,38 @@ export async function getUserPermissionConfig( return parsePermissionGroupConfig(groupMembership.config) } +export async function getPermissionConfig( + userId: string | undefined, + ctx?: ExecutionContext +): Promise { + if (!userId) { + return null + } + + if (ctx) { + if (ctx.permissionConfigLoaded) { + return ctx.permissionConfig ?? null + } + + const config = await getUserPermissionConfig(userId) + ctx.permissionConfig = config + ctx.permissionConfigLoaded = true + return config + } + + return getUserPermissionConfig(userId) +} + export async function validateModelProvider( userId: string | undefined, - model: string + model: string, + ctx?: ExecutionContext ): Promise { if (!userId) { return } - const config = await getUserPermissionConfig(userId) + const config = await getPermissionConfig(userId, ctx) if (!config || config.allowedModelProviders === null) { return @@ -102,13 +126,14 @@ export async function validateModelProvider( export async function validateBlockType( userId: string | undefined, - blockType: string + blockType: string, + ctx?: ExecutionContext ): Promise { if (!userId) { return } - const config = await getUserPermissionConfig(userId) + const config = await getPermissionConfig(userId, ctx) if (!config || config.allowedIntegrations === null) { return @@ -120,12 +145,15 @@ export async function validateBlockType( } } -export async function validateMcpToolsAllowed(userId: string | undefined): Promise { +export async function validateMcpToolsAllowed( + userId: string | undefined, + ctx?: ExecutionContext +): Promise { if (!userId) { return } - const config = await getUserPermissionConfig(userId) + const config = await getPermissionConfig(userId, ctx) if (!config) { return @@ -137,12 +165,15 @@ export async function validateMcpToolsAllowed(userId: string | undefined): Promi } } -export async function validateCustomToolsAllowed(userId: string | undefined): Promise { +export async function validateCustomToolsAllowed( + userId: string | undefined, + ctx?: ExecutionContext +): Promise { if (!userId) { return } - const config = await getUserPermissionConfig(userId) + const config = await getPermissionConfig(userId, ctx) if (!config) { return From 101eace2576a33d300bcd5b872ee022f085ad09e Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 18:13:17 -0800 Subject: [PATCH 12/14] regen migrations --- .../[id]/members/bulk/route.ts | 82 ++++++++++--------- .../permission-groups/[id]/members/route.ts | 48 ++++++----- ...absurd_sumo.sql => 0137_yellow_korath.sql} | 3 +- .../db/migrations/meta/0137_snapshot.json | 27 +----- packages/db/migrations/meta/_journal.json | 4 +- packages/db/schema.ts | 6 +- 6 files changed, 73 insertions(+), 97 deletions(-) rename packages/db/migrations/{0137_absurd_sumo.sql => 0137_yellow_korath.sql} (89%) diff --git a/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts b/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts index dc4a94528e..c6e3faa2d2 100644 --- a/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/members/bulk/route.ts @@ -1,7 +1,7 @@ import { db } from '@sim/db' import { member, permissionGroup, permissionGroupMember } from '@sim/db/schema' import { createLogger } from '@sim/logger' -import { and, eq, inArray, ne } from 'drizzle-orm' +import { and, eq, inArray } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { getSession } from '@/lib/auth' @@ -95,54 +95,49 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: return NextResponse.json({ added: 0, moved: 0 }) } - const existingInThisGroup = await db - .select({ userId: permissionGroupMember.userId }) - .from(permissionGroupMember) - .where( - and( - eq(permissionGroupMember.permissionGroupId, id), - inArray(permissionGroupMember.userId, targetUserIds) - ) - ) - - const existingUserIds = new Set(existingInThisGroup.map((m) => m.userId)) - const usersToAdd = targetUserIds.filter((uid) => !existingUserIds.has(uid)) - - if (usersToAdd.length === 0) { - return NextResponse.json({ added: 0, moved: 0 }) - } - - const otherGroupMemberships = await db + const existingMemberships = await db .select({ id: permissionGroupMember.id, userId: permissionGroupMember.userId, + permissionGroupId: permissionGroupMember.permissionGroupId, }) .from(permissionGroupMember) - .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) - .where( - and( - eq(permissionGroup.organizationId, result.group.organizationId), - inArray(permissionGroupMember.userId, usersToAdd), - ne(permissionGroupMember.permissionGroupId, id) - ) - ) + .where(inArray(permissionGroupMember.userId, targetUserIds)) - const movedCount = otherGroupMemberships.length + const alreadyInThisGroup = new Set( + existingMemberships.filter((m) => m.permissionGroupId === id).map((m) => m.userId) + ) + const usersToAdd = targetUserIds.filter((uid) => !alreadyInThisGroup.has(uid)) - if (otherGroupMemberships.length > 0) { - const idsToDelete = otherGroupMemberships.map((m) => m.id) - await db.delete(permissionGroupMember).where(inArray(permissionGroupMember.id, idsToDelete)) + if (usersToAdd.length === 0) { + return NextResponse.json({ added: 0, moved: 0 }) } - const newMembers = usersToAdd.map((userId) => ({ - id: crypto.randomUUID(), - permissionGroupId: id, - userId, - assignedBy: session.user.id, - assignedAt: new Date(), - })) + const membershipsToDelete = existingMemberships.filter( + (m) => m.permissionGroupId !== id && usersToAdd.includes(m.userId) + ) + const movedCount = membershipsToDelete.length + + await db.transaction(async (tx) => { + if (membershipsToDelete.length > 0) { + await tx.delete(permissionGroupMember).where( + inArray( + permissionGroupMember.id, + membershipsToDelete.map((m) => m.id) + ) + ) + } - await db.insert(permissionGroupMember).values(newMembers) + const newMembers = usersToAdd.map((userId) => ({ + id: crypto.randomUUID(), + permissionGroupId: id, + userId, + assignedBy: session.user.id, + assignedAt: new Date(), + })) + + await tx.insert(permissionGroupMember).values(newMembers) + }) logger.info('Bulk added members to permission group', { permissionGroupId: id, @@ -156,6 +151,15 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: if (error instanceof z.ZodError) { return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) } + if ( + error instanceof Error && + error.message.includes('permission_group_member_user_id_unique') + ) { + return NextResponse.json( + { error: 'One or more users are already in a permission group' }, + { status: 409 } + ) + } logger.error('Error bulk adding members to permission group', error) return NextResponse.json({ error: 'Failed to add members' }, { status: 500 }) } diff --git a/apps/sim/app/api/permission-groups/[id]/members/route.ts b/apps/sim/app/api/permission-groups/[id]/members/route.ts index 172472f54b..4979da755e 100644 --- a/apps/sim/app/api/permission-groups/[id]/members/route.ts +++ b/apps/sim/app/api/permission-groups/[id]/members/route.ts @@ -110,29 +110,27 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: ) } + const [existingMembership] = await db + .select({ + id: permissionGroupMember.id, + permissionGroupId: permissionGroupMember.permissionGroupId, + }) + .from(permissionGroupMember) + .where(eq(permissionGroupMember.userId, userId)) + .limit(1) + + if (existingMembership?.permissionGroupId === id) { + return NextResponse.json( + { error: 'User is already in this permission group' }, + { status: 409 } + ) + } + const newMember = await db.transaction(async (tx) => { - const existingMembership = await tx - .select({ - id: permissionGroupMember.id, - permissionGroupId: permissionGroupMember.permissionGroupId, - }) - .from(permissionGroupMember) - .innerJoin(permissionGroup, eq(permissionGroupMember.permissionGroupId, permissionGroup.id)) - .where( - and( - eq(permissionGroupMember.userId, userId), - eq(permissionGroup.organizationId, result.group.organizationId) - ) - ) - .limit(1) - - if (existingMembership.length > 0) { - if (existingMembership[0].permissionGroupId === id) { - throw new Error('ALREADY_IN_GROUP') - } + if (existingMembership) { await tx .delete(permissionGroupMember) - .where(eq(permissionGroupMember.id, existingMembership[0].id)) + .where(eq(permissionGroupMember.id, existingMembership.id)) } const memberData = { @@ -158,11 +156,11 @@ export async function POST(req: NextRequest, { params }: { params: Promise<{ id: if (error instanceof z.ZodError) { return NextResponse.json({ error: error.errors[0].message }, { status: 400 }) } - if (error instanceof Error && error.message === 'ALREADY_IN_GROUP') { - return NextResponse.json( - { error: 'User is already in this permission group' }, - { status: 409 } - ) + if ( + error instanceof Error && + error.message.includes('permission_group_member_user_id_unique') + ) { + return NextResponse.json({ error: 'User is already in a permission group' }, { status: 409 }) } logger.error('Error adding member to permission group', error) return NextResponse.json({ error: 'Failed to add member' }, { status: 500 }) diff --git a/packages/db/migrations/0137_absurd_sumo.sql b/packages/db/migrations/0137_yellow_korath.sql similarity index 89% rename from packages/db/migrations/0137_absurd_sumo.sql rename to packages/db/migrations/0137_yellow_korath.sql index 8f85a66b71..3b6017095c 100644 --- a/packages/db/migrations/0137_absurd_sumo.sql +++ b/packages/db/migrations/0137_yellow_korath.sql @@ -26,5 +26,4 @@ CREATE INDEX "permission_group_organization_id_idx" ON "permission_group" USING CREATE INDEX "permission_group_created_by_idx" ON "permission_group" USING btree ("created_by");--> statement-breakpoint CREATE UNIQUE INDEX "permission_group_org_name_unique" ON "permission_group" USING btree ("organization_id","name");--> statement-breakpoint CREATE INDEX "permission_group_member_group_id_idx" ON "permission_group_member" USING btree ("permission_group_id");--> statement-breakpoint -CREATE INDEX "permission_group_member_user_id_idx" ON "permission_group_member" USING btree ("user_id");--> statement-breakpoint -CREATE UNIQUE INDEX "permission_group_member_unique" ON "permission_group_member" USING btree ("permission_group_id","user_id"); \ No newline at end of file +CREATE UNIQUE INDEX "permission_group_member_user_id_unique" ON "permission_group_member" USING btree ("user_id"); \ No newline at end of file diff --git a/packages/db/migrations/meta/0137_snapshot.json b/packages/db/migrations/meta/0137_snapshot.json index 93a3b83f9a..4764a4d8e4 100644 --- a/packages/db/migrations/meta/0137_snapshot.json +++ b/packages/db/migrations/meta/0137_snapshot.json @@ -1,5 +1,5 @@ { - "id": "60200982-7693-47b9-a396-c2ffad54f529", + "id": "7bffede1-bcac-4f13-9039-7236379e5b63", "prevId": "ff6b124c-50b5-4e92-bbe3-0b162a184d5b", "version": "7", "dialect": "postgresql", @@ -4267,8 +4267,8 @@ "method": "btree", "with": {} }, - "permission_group_member_user_id_idx": { - "name": "permission_group_member_user_id_idx", + "permission_group_member_user_id_unique": { + "name": "permission_group_member_user_id_unique", "columns": [ { "expression": "user_id", @@ -4277,27 +4277,6 @@ "nulls": "last" } ], - "isUnique": false, - "concurrently": false, - "method": "btree", - "with": {} - }, - "permission_group_member_unique": { - "name": "permission_group_member_unique", - "columns": [ - { - "expression": "permission_group_id", - "isExpression": false, - "asc": true, - "nulls": "last" - }, - { - "expression": "user_id", - "isExpression": false, - "asc": true, - "nulls": "last" - } - ], "isUnique": true, "concurrently": false, "method": "btree", diff --git a/packages/db/migrations/meta/_journal.json b/packages/db/migrations/meta/_journal.json index b8fe5b2b22..87e0df3a8b 100644 --- a/packages/db/migrations/meta/_journal.json +++ b/packages/db/migrations/meta/_journal.json @@ -957,8 +957,8 @@ { "idx": 137, "version": "7", - "when": 1767922492454, - "tag": "0137_absurd_sumo", + "when": 1767924777319, + "tag": "0137_yellow_korath", "breakpoints": true } ] diff --git a/packages/db/schema.ts b/packages/db/schema.ts index 33a514f3a4..ddd1775077 100644 --- a/packages/db/schema.ts +++ b/packages/db/schema.ts @@ -1919,10 +1919,6 @@ export const permissionGroupMember = pgTable( }, (table) => ({ permissionGroupIdIdx: index('permission_group_member_group_id_idx').on(table.permissionGroupId), - userIdIdx: index('permission_group_member_user_id_idx').on(table.userId), - uniqueMembership: uniqueIndex('permission_group_member_unique').on( - table.permissionGroupId, - table.userId - ), + userIdUnique: uniqueIndex('permission_group_member_user_id_unique').on(table.userId), }) ) From eba43d26d34877d841040f3db25206753afe5d53 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Thu, 8 Jan 2026 20:25:47 -0800 Subject: [PATCH 13/14] fix default model picking based on user config --- .../components/combobox/combobox.tsx | 34 ++++++++++++++----- .../components/search-modal/search-modal.tsx | 19 +++++++---- 2 files changed, 38 insertions(+), 15 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx index 31cb326ad8..0565fb998c 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx @@ -135,7 +135,7 @@ export function ComboBox({ const value = isPreview ? previewValue : propValue !== undefined ? propValue : storeValue // Permission-based filtering for model dropdowns - const { isProviderAllowed } = usePermissionConfig() + const { isProviderAllowed, isLoading: isPermissionLoading } = usePermissionConfig() // Evaluate static options if provided as a function const staticOptions = useMemo(() => { @@ -269,16 +269,34 @@ export function ComboBox({ setStoreInitialized(true) }, []) - // Set default value once store is initialized and value is undefined + // Check if current value is valid (exists in allowed options) + const isValueValid = useMemo(() => { + if (value === null || value === undefined) return false + return evaluatedOptions.some((opt) => getOptionValue(opt) === value) + }, [value, evaluatedOptions, getOptionValue]) + + // Set default value once store is initialized and permissions are loaded + // Also reset if current value becomes invalid (e.g., provider was blocked) useEffect(() => { - if ( - storeInitialized && - (value === null || value === undefined) && - defaultOptionValue !== undefined - ) { + if (isPermissionLoading) return + if (!storeInitialized) return + if (defaultOptionValue === undefined) return + + const needsDefault = value === null || value === undefined + const needsReset = subBlockId === 'model' && value && !isValueValid + + if (needsDefault || needsReset) { setStoreValue(defaultOptionValue) } - }, [storeInitialized, value, defaultOptionValue, setStoreValue]) + }, [ + storeInitialized, + value, + defaultOptionValue, + setStoreValue, + isPermissionLoading, + subBlockId, + isValueValid, + ]) // Clear fetched options and hydrated option when dependencies change useEffect(() => { diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx index 2eed14889a..ee43fefbf9 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-modal.tsx @@ -12,6 +12,7 @@ import { getTriggersForSidebar, hasTriggerCapability } from '@/lib/workflows/tri import { searchItems } from '@/app/workspace/[workspaceId]/w/components/sidebar/components/search-modal/search-utils' import { SIDEBAR_SCROLL_EVENT } from '@/app/workspace/[workspaceId]/w/components/sidebar/sidebar' import { getAllBlocks } from '@/blocks' +import { usePermissionConfig } from '@/hooks/use-permission-config' interface SearchModalProps { open: boolean @@ -99,12 +100,14 @@ export function SearchModal({ const router = useRouter() const workspaceId = params.workspaceId as string const brand = useBrandConfig() + const { filterBlocks } = usePermissionConfig() const blocks = useMemo(() => { if (!isOnWorkflowPage) return [] const allBlocks = getAllBlocks() - const regularBlocks = allBlocks + const filteredAllBlocks = filterBlocks(allBlocks) + const regularBlocks = filteredAllBlocks .filter( (block) => block.type !== 'starter' && !block.hideFromToolbar && block.category === 'blocks' ) @@ -138,16 +141,17 @@ export function SearchModal({ }, ] - return [...regularBlocks, ...specialBlocks] - }, [isOnWorkflowPage]) + return [...regularBlocks, ...filterBlocks(specialBlocks)] + }, [isOnWorkflowPage, filterBlocks]) const triggers = useMemo(() => { if (!isOnWorkflowPage) return [] const allTriggers = getTriggersForSidebar() + const filteredTriggers = filterBlocks(allTriggers) const priorityOrder = ['Start', 'Schedule', 'Webhook'] - const sortedTriggers = allTriggers.sort((a, b) => { + const sortedTriggers = filteredTriggers.sort((a, b) => { const aIndex = priorityOrder.indexOf(a.name) const bIndex = priorityOrder.indexOf(b.name) const aHasPriority = aIndex !== -1 @@ -170,13 +174,14 @@ export function SearchModal({ config: block, }) ) - }, [isOnWorkflowPage]) + }, [isOnWorkflowPage, filterBlocks]) const tools = useMemo(() => { if (!isOnWorkflowPage) return [] const allBlocks = getAllBlocks() - return allBlocks + const filteredAllBlocks = filterBlocks(allBlocks) + return filteredAllBlocks .filter((block) => block.category === 'tools') .map( (block): ToolItem => ({ @@ -188,7 +193,7 @@ export function SearchModal({ type: block.type, }) ) - }, [isOnWorkflowPage]) + }, [isOnWorkflowPage, filterBlocks]) const pages = useMemo( (): PageItem[] => [ From 22e75064fdee28b0f0d0aa797ae01e15507e1461 Mon Sep 17 00:00:00 2001 From: Vikhyath Mondreti Date: Fri, 9 Jan 2026 20:14:41 -0800 Subject: [PATCH 14/14] cleaned up UI --- .../access-control/access-control.tsx | 1142 +++++++++-------- .../components/api-keys/api-keys.tsx | 2 +- .../settings-modal/components/byok/byok.tsx | 4 +- .../components/copilot/copilot.tsx | 2 +- .../credential-sets/credential-sets.tsx | 4 +- .../components/custom-tools/custom-tools.tsx | 4 +- .../formatted-input/formatted-input.tsx | 66 - .../mcp/components/header-row/header-row.tsx | 81 -- .../components/mcp/components/index.ts | 10 - .../server-list-item/server-list-item.tsx | 76 -- .../components/mcp/components/types.ts | 35 - .../settings-modal/components/mcp/mcp.tsx | 273 +++- .../workflow-mcp-servers.tsx | 8 +- .../settings-modal/settings-modal.tsx | 16 +- apps/sim/lib/core/config/feature-flags.ts | 6 +- 15 files changed, 884 insertions(+), 845 deletions(-) delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/formatted-input/formatted-input.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/header-row/header-row.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx delete mode 100644 apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/types.ts diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx index 44beda97d5..86d65cd038 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/access-control/access-control.tsx @@ -2,7 +2,7 @@ import { useCallback, useMemo, useState } from 'react' import { createLogger } from '@sim/logger' -import { Check, Plus, Search } from 'lucide-react' +import { Plus, Search } from 'lucide-react' import { Avatar, AvatarFallback, @@ -16,11 +16,14 @@ import { ModalContent, ModalFooter, ModalHeader, + ModalTabs, + ModalTabsContent, + ModalTabsList, + ModalTabsTrigger, } from '@/components/emcn' import { Input as BaseInput, Skeleton } from '@/components/ui' import { useSession } from '@/lib/auth/auth-client' import { getSubscriptionStatus } from '@/lib/billing/client' -import { cn } from '@/lib/core/utils/cn' import type { PermissionGroupConfig } from '@/lib/permission-groups/types' import { getUserRole } from '@/lib/workspaces/organization' import { getUserColor } from '@/app/workspace/[workspaceId]/w/utils/get-user-color' @@ -37,10 +40,180 @@ import { useUpdatePermissionGroup, } from '@/hooks/queries/permission-groups' import { useSubscriptionData } from '@/hooks/queries/subscription' +import { PROVIDER_DEFINITIONS } from '@/providers/models' import { getAllProviderIds } from '@/providers/utils' const logger = createLogger('AccessControl') +interface AddMembersModalProps { + open: boolean + onOpenChange: (open: boolean) => void + availableMembers: any[] + selectedMemberIds: Set + setSelectedMemberIds: React.Dispatch>> + onAddMembers: () => void + isAdding: boolean +} + +function AddMembersModal({ + open, + onOpenChange, + availableMembers, + selectedMemberIds, + setSelectedMemberIds, + onAddMembers, + isAdding, +}: AddMembersModalProps) { + const [searchTerm, setSearchTerm] = useState('') + + const filteredMembers = useMemo(() => { + if (!searchTerm.trim()) return availableMembers + const query = searchTerm.toLowerCase() + return availableMembers.filter((m: any) => { + const name = m.user?.name || '' + const email = m.user?.email || '' + return name.toLowerCase().includes(query) || email.toLowerCase().includes(query) + }) + }, [availableMembers, searchTerm]) + + const allFilteredSelected = useMemo(() => { + if (filteredMembers.length === 0) return false + return filteredMembers.every((m: any) => selectedMemberIds.has(m.userId)) + }, [filteredMembers, selectedMemberIds]) + + const handleToggleAll = () => { + if (allFilteredSelected) { + const filteredIds = new Set(filteredMembers.map((m: any) => m.userId)) + setSelectedMemberIds((prev) => { + const next = new Set(prev) + filteredIds.forEach((id) => next.delete(id)) + return next + }) + } else { + setSelectedMemberIds((prev) => { + const next = new Set(prev) + filteredMembers.forEach((m: any) => next.add(m.userId)) + return next + }) + } + } + + const handleToggleMember = (userId: string) => { + setSelectedMemberIds((prev) => { + const next = new Set(prev) + if (next.has(userId)) { + next.delete(userId) + } else { + next.add(userId) + } + return next + }) + } + + return ( + { + if (!o) setSearchTerm('') + onOpenChange(o) + }} + > + + Add Members + + {availableMembers.length === 0 ? ( +

+ All organization members are already in this group. +

+ ) : ( +
+
+
+ + setSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-[13px] leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + /> +
+ +
+ +
+ {filteredMembers.length === 0 ? ( +

+ No members found matching "{searchTerm}" +

+ ) : ( +
+ {filteredMembers.map((member: any) => { + const name = member.user?.name || 'Unknown' + const email = member.user?.email || '' + const avatarInitial = name.charAt(0).toUpperCase() + const isSelected = selectedMemberIds.has(member.userId) + + return ( + + ) + })} +
+ )} +
+
+ )} +
+ + + + +
+
+ ) +} + function AccessControlSkeleton() { return (
@@ -110,6 +283,93 @@ export function AccessControl() { const [selectedMemberIds, setSelectedMemberIds] = useState>(new Set()) const [providerSearchTerm, setProviderSearchTerm] = useState('') const [integrationSearchTerm, setIntegrationSearchTerm] = useState('') + const [platformSearchTerm, setPlatformSearchTerm] = useState('') + const [showUnsavedChanges, setShowUnsavedChanges] = useState(false) + + const platformFeatures = useMemo( + () => [ + { + id: 'hide-knowledge-base', + label: 'Knowledge Base', + category: 'Sidebar', + configKey: 'hideKnowledgeBaseTab' as const, + }, + { + id: 'hide-templates', + label: 'Templates', + category: 'Sidebar', + configKey: 'hideTemplates' as const, + }, + { + id: 'hide-copilot', + label: 'Copilot', + category: 'Workflow Panel', + configKey: 'hideCopilot' as const, + }, + { + id: 'hide-api-keys', + label: 'API Keys', + category: 'Settings Tabs', + configKey: 'hideApiKeysTab' as const, + }, + { + id: 'hide-environment', + label: 'Environment', + category: 'Settings Tabs', + configKey: 'hideEnvironmentTab' as const, + }, + { + id: 'hide-files', + label: 'Files', + category: 'Settings Tabs', + configKey: 'hideFilesTab' as const, + }, + { + id: 'disable-mcp', + label: 'MCP Tools', + category: 'Tools', + configKey: 'disableMcpTools' as const, + }, + { + id: 'disable-custom-tools', + label: 'Custom Tools', + category: 'Tools', + configKey: 'disableCustomTools' as const, + }, + { + id: 'hide-trace-spans', + label: 'Trace Spans', + category: 'Logs', + configKey: 'hideTraceSpans' as const, + }, + ], + [] + ) + + const filteredPlatformFeatures = useMemo(() => { + if (!platformSearchTerm.trim()) return platformFeatures + const search = platformSearchTerm.toLowerCase() + return platformFeatures.filter( + (f) => f.label.toLowerCase().includes(search) || f.category.toLowerCase().includes(search) + ) + }, [platformFeatures, platformSearchTerm]) + + const platformCategories = useMemo(() => { + const categories: Record = {} + for (const feature of filteredPlatformFeatures) { + if (!categories[feature.category]) { + categories[feature.category] = [] + } + categories[feature.category].push(feature) + } + return categories + }, [filteredPlatformFeatures]) + + const hasConfigChanges = useMemo(() => { + if (!viewingGroup || !editingConfig) return false + const original = viewingGroup.config + return JSON.stringify(original) !== JSON.stringify(editingConfig) + }, [viewingGroup, editingConfig]) const allBlocks = useMemo(() => { // Filter out hidden blocks and start_trigger (which should never be disabled) @@ -240,6 +500,7 @@ export function AccessControl() { setEditingConfig(null) setProviderSearchTerm('') setIntegrationSearchTerm('') + setPlatformSearchTerm('') setViewingGroup((prev) => (prev ? { ...prev, config: editingConfig } : null)) } catch (error) { logger.error('Failed to update config', error) @@ -349,110 +610,96 @@ export function AccessControl() { return ( <>
+
+
+

+ {viewingGroup.name} +

+ +
+ {viewingGroup.description && ( +

{viewingGroup.description}

+ )} +
+
-
-
- - Group Name - - - {viewingGroup.name} - -
- {viewingGroup.description && ( - <> -
- - {viewingGroup.description} - - - )} -
-
-
-
-

Members

- -
- - {membersLoading ? ( -
- {[1, 2].map((i) => ( -
-
- -
- - -
+ {membersLoading ? ( +
+ {[1, 2].map((i) => ( +
+
+ +
+ +
- ))} -
- ) : members.length === 0 ? ( -

- No members yet. Add members using the buttons above. -

- ) : ( -
- {members.map((member) => { - const name = member.userName || 'Unknown' - const avatarInitial = name.charAt(0).toUpperCase() +
+ ))} +
+ ) : members.length === 0 ? ( +

+ No members yet. Click "Add" to get started. +

+ ) : ( +
+ {members.map((member) => { + const name = member.userName || 'Unknown' + const avatarInitial = name.charAt(0).toUpperCase() - return ( -
-
- - {member.userImage && ( - - )} - - {avatarInitial} - - + return ( +
+
+ + {member.userImage && } + + {avatarInitial} + + -
-
- - {name} - -
-
- {member.userEmail} -
+
+
+ + {name} + +
+
+ {member.userEmail}
-
- -
-
- ) - })} -
- )} -
+ + +
+ ) + })} +
+ )}
@@ -463,382 +710,245 @@ export function AccessControl() {
- + { + if (!open && hasConfigChanges) { + setShowUnsavedChanges(true) + } else { + setShowConfigModal(open) + if (!open) { + setProviderSearchTerm('') + setIntegrationSearchTerm('') + setPlatformSearchTerm('') + } + } + }} + > Configure Permissions - -
-
-
- - -
-

- Select which model providers are available in agent dropdowns. All are allowed - by default. -

-
- - setProviderSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-[13px] leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
-
- {filteredProviders.map((providerId) => ( - - ))} -
-
+ + + Model Providers + Blocks + Platform + -
-
- - -
-

- Select which blocks are visible in the toolbar. All are visible by default. -

-
- - setIntegrationSearchTerm(e.target.value)} - className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-[13px] leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' - /> -
-
- {filteredBlocks.map((block) => ( - - ))} -
-
- -
-
- - +
+
+ {filteredProviders.map((providerId) => { + const ProviderIcon = PROVIDER_DEFINITIONS[providerId]?.icon + const providerName = + PROVIDER_DEFINITIONS[providerId]?.name || + providerId.replace(/-/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()) + return ( +
+ toggleProvider(providerId)} + /> +
+ {ProviderIcon && } +
+ {providerName} +
) - }} - className='text-[12px] text-[var(--accent)] hover:underline' - > - {!editingConfig?.hideKnowledgeBaseTab && - !editingConfig?.hideTemplates && - !editingConfig?.hideCopilot && - !editingConfig?.hideApiKeysTab && - !editingConfig?.hideEnvironmentTab && - !editingConfig?.hideFilesTab && - !editingConfig?.disableMcpTools && - !editingConfig?.disableCustomTools && - !editingConfig?.hideTraceSpans - ? 'Deselect All' - : 'Select All'} - -
-

- Checked features are visible. Uncheck to hide. -

-
- {/* Sidebar */} -
- - Sidebar - -
-
- - setEditingConfig((prev) => - prev ? { ...prev, hideKnowledgeBaseTab: checked !== true } : prev - ) - } - /> - -
-
- - setEditingConfig((prev) => - prev ? { ...prev, hideTemplates: checked !== true } : prev - ) - } - /> - -
-
+ })}
+
+ + - {/* Workflow Panel */} -
- - Workflow Panel - -
-
- - setEditingConfig((prev) => - prev ? { ...prev, hideCopilot: checked !== true } : prev - ) - } - /> - -
+ + +
+
+
+ + setIntegrationSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-[13px] leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
+
- - {/* Settings Tabs */} -
- - Settings Tabs - -
-
- - setEditingConfig((prev) => - prev ? { ...prev, hideApiKeysTab: checked !== true } : prev - ) - } - /> - -
-
- - setEditingConfig((prev) => - prev ? { ...prev, hideEnvironmentTab: checked !== true } : prev - ) - } - /> - -
-
- - setEditingConfig((prev) => - prev ? { ...prev, hideFilesTab: checked !== true } : prev - ) - } - /> - -
-
+
+ {filteredBlocks.map((block) => { + const BlockIcon = block.icon + return ( +
+ toggleIntegration(block.type)} + /> +
+ {BlockIcon && ( + + )} +
+ {block.name} +
+ ) + })}
+
+ + - {/* Tools */} -
- - Tools - -
-
- - setEditingConfig((prev) => - prev ? { ...prev, disableMcpTools: checked !== true } : prev - ) - } - /> - -
-
- - setEditingConfig((prev) => - prev ? { ...prev, disableCustomTools: checked !== true } : prev - ) - } - /> - -
+ + +
+
+
+ + setPlatformSearchTerm(e.target.value)} + className='h-auto flex-1 border-0 bg-transparent p-0 font-base text-[13px] leading-none placeholder:text-[var(--text-tertiary)] focus-visible:ring-0 focus-visible:ring-offset-0' + />
+
- - {/* Logs */} -
- - Logs - -
-
- - setEditingConfig((prev) => - prev ? { ...prev, hideTraceSpans: checked !== true } : prev - ) - } - /> - +
+ {Object.entries(platformCategories).map(([category, features]) => ( +
+ + {category} + +
+ {features.map((feature) => ( +
+ + setEditingConfig((prev) => + prev + ? { ...prev, [feature.configKey]: checked !== true } + : prev + ) + } + /> + +
+ ))} +
-
+ ))}
-
-
-
+ +
+ @@ -854,103 +964,51 @@ export function AccessControl() { - - - Add Members - - {availableMembersToAdd.length === 0 ? ( -

- All organization members are already in this group. -

- ) : ( -
-
- -
- {availableMembersToAdd.map((member: any) => { - const name = member.user?.name || 'Unknown' - const email = member.user?.email || '' - const avatarInitial = name.charAt(0).toUpperCase() - const isSelected = selectedMemberIds.has(member.userId) - - return ( - - ) - })} -
- )} + + + Unsaved Changes + +

+ You have unsaved changes. Do you want to save them before closing? +

-
+ + ) } @@ -983,8 +1041,8 @@ export function AccessControl() { No results found matching "{searchTerm}"
) : permissionGroups.length === 0 ? ( -
- No permission groups created yet +
+ Click "Create" above to get started
) : (
@@ -996,12 +1054,12 @@ export function AccessControl() { {group.memberCount} member{group.memberCount !== 1 ? 's' : ''}
-
-
{existingKey ? (
-
-
- -
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/index.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/index.ts index 5862dd823b..a044207bda 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/index.ts @@ -1,12 +1,2 @@ export { FormField } from './form-field/form-field' -export { FormattedInput } from './formatted-input/formatted-input' -export { HeaderRow } from './header-row/header-row' export { McpServerSkeleton } from './mcp-server-skeleton/mcp-server-skeleton' -export { formatTransportLabel, ServerListItem } from './server-list-item/server-list-item' -export type { - EnvVarDropdownConfig, - HeaderEntry, - InputFieldType, - McpServerFormData, - McpServerTestResult, -} from './types' diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx deleted file mode 100644 index c5ad6f8098..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/server-list-item/server-list-item.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Button } from '@/components/emcn' - -export function formatTransportLabel(transport: string): string { - return transport - .split('-') - .map((word) => - ['http', 'sse', 'stdio'].includes(word.toLowerCase()) - ? word.toUpperCase() - : word.charAt(0).toUpperCase() + word.slice(1) - ) - .join('-') -} - -function formatToolsLabel(tools: any[], connectionStatus?: string): string { - if (connectionStatus === 'error') { - return 'Unable to connect' - } - const count = tools.length - const plural = count !== 1 ? 's' : '' - const names = count > 0 ? `: ${tools.map((t) => t.name).join(', ')}` : '' - return `${count} tool${plural}${names}` -} - -interface ServerListItemProps { - server: any - tools: any[] - isDeleting: boolean - isLoadingTools?: boolean - isRefreshing?: boolean - onRemove: () => void - onViewDetails: () => void -} - -export function ServerListItem({ - server, - tools, - isDeleting, - isLoadingTools = false, - isRefreshing = false, - onRemove, - onViewDetails, -}: ServerListItemProps) { - const transportLabel = formatTransportLabel(server.transport || 'http') - const toolsLabel = formatToolsLabel(tools, server.connectionStatus) - const isError = server.connectionStatus === 'error' - - return ( -
-
-
- - {server.name || 'Unnamed Server'} - - ({transportLabel}) -
-

- {isRefreshing - ? 'Refreshing...' - : isLoadingTools && tools.length === 0 - ? 'Loading...' - : toolsLabel} -

-
-
- - -
-
- ) -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/types.ts b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/types.ts deleted file mode 100644 index 5b1bfd90d9..0000000000 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/components/types.ts +++ /dev/null @@ -1,35 +0,0 @@ -import type { McpTransport } from '@/lib/mcp/types' - -/** - * Represents a single header entry in the form. - * Using an array of objects allows duplicate keys during editing. - */ -export interface HeaderEntry { - key: string - value: string -} - -export interface McpServerFormData { - name: string - transport: McpTransport - url?: string - timeout?: number - headers?: HeaderEntry[] -} - -export interface McpServerTestResult { - success: boolean - message?: string - error?: string - warnings?: string[] -} - -export type InputFieldType = 'url' | 'header-key' | 'header-value' - -export interface EnvVarDropdownConfig { - searchTerm: string - cursorPosition: number - workspaceId: string - onSelect: (value: string) => void - onClose: () => void -} diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/mcp.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/mcp.tsx index beab9c41a5..c4b8aa9d0f 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/mcp.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/mcp/mcp.tsx @@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { createLogger } from '@sim/logger' -import { Plus, Search } from 'lucide-react' +import { Plus, Search, X } from 'lucide-react' import { useParams } from 'next/navigation' import { Badge, @@ -16,13 +16,19 @@ import { Tooltip, } from '@/components/emcn' import { Input } from '@/components/ui' +import { cn } from '@/lib/core/utils/cn' import { getIssueBadgeLabel, getIssueBadgeVariant, getMcpToolIssue, type McpToolIssue, } from '@/lib/mcp/tool-validation' -import { checkEnvVarTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' +import type { McpTransport } from '@/lib/mcp/types' +import { + checkEnvVarTrigger, + EnvVarDropdown, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/env-var-dropdown' +import { formatDisplayText } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/formatted-text' import { useCreateMcpServer, useDeleteMcpServer, @@ -35,15 +41,41 @@ import { import { useMcpServerTest } from '@/hooks/use-mcp-server-test' import { useWorkflowRegistry } from '@/stores/workflows/registry/store' import { useSubBlockStore } from '@/stores/workflows/subblock/store' -import type { InputFieldType, McpServerFormData, McpServerTestResult } from './components' -import { - FormattedInput, - FormField, - formatTransportLabel, - HeaderRow, - McpServerSkeleton, - ServerListItem, -} from './components' +import { FormField, McpServerSkeleton } from './components' + +/** + * Represents a single header entry in the form. + * Using an array of objects allows duplicate keys during editing. + */ +interface HeaderEntry { + key: string + value: string +} + +interface McpServerFormData { + name: string + transport: McpTransport + url?: string + timeout?: number + headers?: HeaderEntry[] +} + +interface McpServerTestResult { + success: boolean + message?: string + error?: string + warnings?: string[] +} + +type InputFieldType = 'url' | 'header-key' | 'header-value' + +interface EnvVarDropdownConfig { + searchTerm: string + cursorPosition: number + workspaceId: string + onSelect: (value: string) => void + onClose: () => void +} interface McpTool { name: string @@ -71,6 +103,33 @@ const DEFAULT_FORM_DATA: McpServerFormData = { headers: [{ key: '', value: '' }], } +/** + * Formats a transport type string for display. + */ +function formatTransportLabel(transport: string): string { + return transport + .split('-') + .map((word) => + ['http', 'sse', 'stdio'].includes(word.toLowerCase()) + ? word.toUpperCase() + : word.charAt(0).toUpperCase() + word.slice(1) + ) + .join('-') +} + +/** + * Formats a tools list for display in the server list. + */ +function formatToolsLabel(tools: McpTool[], connectionStatus?: string): string { + if (connectionStatus === 'error') { + return 'Unable to connect' + } + const count = tools.length + const plural = count !== 1 ? 's' : '' + const names = count > 0 ? `: ${tools.map((t) => t.name).join(', ')}` : '' + return `${count} tool${plural}${names}` +} + /** * Determines the label for the test connection button based on current state. */ @@ -84,6 +143,198 @@ function getTestButtonLabel( return 'Test Connection' } +interface FormattedInputProps { + ref?: React.RefObject + placeholder: string + value: string + scrollLeft: number + showEnvVars: boolean + envVarProps: EnvVarDropdownConfig + className?: string + onChange: (e: React.ChangeEvent) => void + onScroll: (scrollLeft: number) => void +} + +function FormattedInput({ + ref, + placeholder, + value, + scrollLeft, + showEnvVars, + envVarProps, + className, + onChange, + onScroll, +}: FormattedInputProps) { + const handleScroll = (e: React.UIEvent) => { + onScroll(e.currentTarget.scrollLeft) + } + + return ( +
+ +
+
+ {formatDisplayText(value)} +
+
+ {showEnvVars && ( + + )} +
+ ) +} + +interface HeaderRowProps { + header: HeaderEntry + index: number + headerScrollLeft: Record + showEnvVars: boolean + activeInputField: InputFieldType | null + activeHeaderIndex: number | null + envSearchTerm: string + cursorPosition: number + workspaceId: string + onInputChange: (field: InputFieldType, value: string, index?: number) => void + onHeaderScroll: (key: string, scrollLeft: number) => void + onEnvVarSelect: (value: string) => void + onEnvVarClose: () => void + onRemove: () => void +} + +function HeaderRow({ + header, + index, + headerScrollLeft, + showEnvVars, + activeInputField, + activeHeaderIndex, + envSearchTerm, + cursorPosition, + workspaceId, + onInputChange, + onHeaderScroll, + onEnvVarSelect, + onEnvVarClose, + onRemove, +}: HeaderRowProps) { + const isKeyActive = + showEnvVars && activeInputField === 'header-key' && activeHeaderIndex === index + const isValueActive = + showEnvVars && activeInputField === 'header-value' && activeHeaderIndex === index + + const envVarProps: EnvVarDropdownConfig = { + searchTerm: envSearchTerm, + cursorPosition, + workspaceId, + onSelect: onEnvVarSelect, + onClose: onEnvVarClose, + } + + return ( +
+ onInputChange('header-key', e.target.value, index)} + onScroll={(scrollLeft) => onHeaderScroll(`key-${index}`, scrollLeft)} + /> + + onInputChange('header-value', e.target.value, index)} + onScroll={(scrollLeft) => onHeaderScroll(`value-${index}`, scrollLeft)} + /> + + +
+ ) +} + +interface ServerListItemProps { + server: McpServer + tools: McpTool[] + isDeleting: boolean + isLoadingTools?: boolean + isRefreshing?: boolean + onRemove: () => void + onViewDetails: () => void +} + +function ServerListItem({ + server, + tools, + isDeleting, + isLoadingTools = false, + isRefreshing = false, + onRemove, + onViewDetails, +}: ServerListItemProps) { + const transportLabel = formatTransportLabel(server.transport || 'http') + const toolsLabel = formatToolsLabel(tools, server.connectionStatus) + const isError = server.connectionStatus === 'error' + + return ( +
+
+
+ + {server.name || 'Unnamed Server'} + + ({transportLabel}) +
+

+ {isRefreshing + ? 'Refreshing...' + : isLoadingTools && tools.length === 0 + ? 'Loading...' + : toolsLabel} +

+
+
+ + +
+
+ ) +} + interface MCPProps { initialServerId?: string | null } diff --git a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx index 8736bf59d0..0c1b6a4efe 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/components/sidebar/components/settings-modal/components/workflow-mcp-servers/workflow-mcp-servers.tsx @@ -208,11 +208,11 @@ function ServerDetailView({ workspaceId, serverId, onBack }: ServerDetailViewPro

-
-