From 74bc60c01aad0b1e9e5850b0c2b12fbd2ac92a34 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 5 May 2026 21:49:35 -0700 Subject: [PATCH 1/7] fix(mothership): enforce ownership check on workflow resource attachments --- apps/sim/lib/copilot/chat/process-contents.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index 78d9b94db07..b0dfcd4f837 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -701,7 +701,7 @@ export async function resolveActiveResourceContext( resourceType: string, resourceId: string, workspaceId: string, - _userId: string, + userId: string, chatId?: string ): Promise { try { @@ -709,10 +709,10 @@ export async function resolveActiveResourceContext( case 'workflow': { const ctx = await processWorkflowFromDb( resourceId, - undefined, + userId, '@active_resource', 'current_workflow', - undefined, + workspaceId, chatId ) if (!ctx) return null From 58d734c62f6ace3fd0ca2dab4fe07eaffdbd1301 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 5 May 2026 21:54:49 -0700 Subject: [PATCH 2/7] fix(mothership): fix table and knowledgebase BOLA in resource attachment resolution --- apps/sim/lib/copilot/chat/process-contents.ts | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index b0dfcd4f837..1240b053200 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -721,7 +721,7 @@ export async function resolveActiveResourceContext( case 'knowledgebase': { const ctx = await processKnowledgeFromDb( resourceId, - undefined, + userId, '@active_resource', workspaceId ) @@ -729,7 +729,7 @@ export async function resolveActiveResourceContext( return { type: 'active_resource', tag: '@active_resource', content: ctx.content } } case 'table': { - return await resolveTableResource(resourceId) + return await resolveTableResource(resourceId, workspaceId) } case 'file': { return await resolveFileResource(resourceId, workspaceId) @@ -745,9 +745,13 @@ export async function resolveActiveResourceContext( return null } } -async function resolveTableResource(tableId: string): Promise { +async function resolveTableResource( + tableId: string, + workspaceId: string +): Promise { const table = await getTableById(tableId) if (!table) return null + if (table.workspaceId !== workspaceId) return null return { type: 'active_resource', tag: '@active_resource', From 7413b77bf6908a281e995b8051a640df9624faf5 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 5 May 2026 21:55:08 -0700 Subject: [PATCH 3/7] fix(mothership): apply workspace scope to table in processContextsServer --- apps/sim/lib/copilot/chat/process-contents.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/copilot/chat/process-contents.ts b/apps/sim/lib/copilot/chat/process-contents.ts index 1240b053200..d570bab39e7 100644 --- a/apps/sim/lib/copilot/chat/process-contents.ts +++ b/apps/sim/lib/copilot/chat/process-contents.ts @@ -116,8 +116,8 @@ export async function processContextsServer( currentWorkspaceId ) } - if (ctx.kind === 'table' && ctx.tableId) { - const result = await resolveTableResource(ctx.tableId) + if (ctx.kind === 'table' && ctx.tableId && currentWorkspaceId) { + const result = await resolveTableResource(ctx.tableId, currentWorkspaceId) if (!result) return null return { type: 'table', tag: ctx.label ? `@${ctx.label}` : '@', content: result.content } } From bb1e8b64bd7919185c7481d28141ef7e38506085 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 5 May 2026 22:09:23 -0700 Subject: [PATCH 4/7] fix(mothership): verify workspace membership before resolving workspace branch --- apps/sim/lib/copilot/chat/post.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index a745f209c9e..df9faedc875 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -1,9 +1,9 @@ import { type Context as OtelContext, context as otelContextApi } from '@opentelemetry/api' import { db } from '@sim/db' -import { copilotChats } from '@sim/db/schema' +import { copilotChats, permissions } from '@sim/db/schema' import { createLogger } from '@sim/logger' import { generateId } from '@sim/utils/id' -import { eq, sql } from 'drizzle-orm' +import { and, eq, sql } from 'drizzle-orm' import { type NextRequest, NextResponse } from 'next/server' import { z } from 'zod' import { isZodError, validationErrorResponse } from '@/lib/api/server' @@ -569,6 +569,22 @@ async function resolveBranch(params: { return createBadRequestResponse('workspaceId is required when workflowId is not provided') } + const [permissionRow] = await db + .select({ permissionType: permissions.permissionType }) + .from(permissions) + .where( + and( + eq(permissions.userId, authenticatedUserId), + eq(permissions.entityType, 'workspace'), + eq(permissions.entityId, requestedWorkspaceId) + ) + ) + .limit(1) + + if (!permissionRow) { + return createBadRequestResponse('Workspace not found or access denied') + } + return { kind: 'workspace', workspaceId: requestedWorkspaceId, From 2c1b3bba5899492f4b81d8328a31c066d3e2423b Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 5 May 2026 22:10:44 -0700 Subject: [PATCH 5/7] fix(data-drains): use const for timeoutId in sleepUntilAborted --- apps/sim/lib/data-drains/destinations/webhook.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/apps/sim/lib/data-drains/destinations/webhook.ts b/apps/sim/lib/data-drains/destinations/webhook.ts index ba192b9943b..525898e3484 100644 --- a/apps/sim/lib/data-drains/destinations/webhook.ts +++ b/apps/sim/lib/data-drains/destinations/webhook.ts @@ -99,12 +99,11 @@ function sign(body: Buffer, secret: string, timestamp: number): string { function sleepUntilAborted(ms: number, signal: AbortSignal): Promise { if (signal.aborted) return Promise.resolve() return new Promise((resolve) => { - let timeoutId: ReturnType const onAbort = () => { clearTimeout(timeoutId) resolve() } - timeoutId = setTimeout(() => { + const timeoutId = setTimeout(() => { signal.removeEventListener('abort', onAbort) resolve() }, ms) From cb1f9eb9bcd5476b51749c43e466a38cf652b963 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 5 May 2026 22:39:35 -0700 Subject: [PATCH 6/7] fix(test): mock db.select and drizzle and for workspace permissions check --- apps/sim/lib/copilot/chat/post.test.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/apps/sim/lib/copilot/chat/post.test.ts b/apps/sim/lib/copilot/chat/post.test.ts index 3c114eccba2..1a6dee8a4b1 100644 --- a/apps/sim/lib/copilot/chat/post.test.ts +++ b/apps/sim/lib/copilot/chat/post.test.ts @@ -93,10 +93,18 @@ vi.mock('@sim/db', () => ({ })), })), })), + select: vi.fn(() => ({ + from: vi.fn(() => ({ + where: vi.fn(() => ({ + limit: vi.fn().mockResolvedValue([{ permissionType: 'write' }]), + })), + })), + })), }, })) vi.mock('drizzle-orm', () => ({ + and: vi.fn(() => ({})), eq: vi.fn(() => ({})), sql: (strings: TemplateStringsArray, ...values: unknown[]) => ({ strings, values }), })) From 8515dcea69247985550ac31b6e480cd5ae5ed0e2 Mon Sep 17 00:00:00 2001 From: waleed Date: Tue, 5 May 2026 22:59:48 -0700 Subject: [PATCH 7/7] fix(mothership): always derive workspace from workflow record in workflow branch --- apps/sim/lib/copilot/chat/post.ts | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/apps/sim/lib/copilot/chat/post.ts b/apps/sim/lib/copilot/chat/post.ts index df9faedc875..a9d1eb30adc 100644 --- a/apps/sim/lib/copilot/chat/post.ts +++ b/apps/sim/lib/copilot/chat/post.ts @@ -506,14 +506,12 @@ async function resolveBranch(params: { } const resolvedWorkflowId = resolved.workflowId - let resolvedWorkspaceId = requestedWorkspaceId - if (!resolvedWorkspaceId) { - try { - const workflow = await getWorkflowById(resolvedWorkflowId) - resolvedWorkspaceId = workflow?.workspaceId ?? undefined - } catch { - // best effort; downstream calls can still proceed - } + let resolvedWorkspaceId: string | undefined + try { + const workflow = await getWorkflowById(resolvedWorkflowId) + resolvedWorkspaceId = workflow?.workspaceId ?? requestedWorkspaceId + } catch { + resolvedWorkspaceId = requestedWorkspaceId } const selectedModel = model || DEFAULT_MODEL