Skip to content

Commit a664841

Browse files
committed
fix
1 parent c78e614 commit a664841

5 files changed

Lines changed: 123 additions & 14 deletions

File tree

apps/sim/lib/copilot/chat/post.test.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { NextRequest } from 'next/server'
1313
import { beforeEach, describe, expect, it, vi } from 'vitest'
1414

1515
const resolveWorkflowIdForUser = workflowsUtilsMockFns.mockResolveWorkflowIdForUser
16-
const getWorkflowById = workflowsUtilsMockFns.mockGetWorkflowById
1716
const getUserEntityPermissions = permissionsMockFns.mockGetUserEntityPermissions
1817

1918
const {
@@ -138,9 +137,9 @@ describe('handleUnifiedChatPost', () => {
138137
resolveWorkflowIdForUser.mockResolvedValue({
139138
status: 'resolved',
140139
workflowId: 'wf-1',
140+
workspaceId: 'ws-1',
141141
workflowName: 'Workflow One',
142142
})
143-
getWorkflowById.mockResolvedValue({ workspaceId: 'ws-1' })
144143
getUserEntityPermissions.mockResolvedValue('write')
145144
getEffectiveDecryptedEnv.mockResolvedValue({ API_KEY: 'secret' })
146145
generateWorkspaceContext.mockResolvedValue('workspace context')

apps/sim/lib/copilot/chat/post.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ import { persistChatResources } from '@/lib/copilot/resources/persistence'
4444
import { taskPubSub } from '@/lib/copilot/tasks'
4545
import { prepareExecutionContext } from '@/lib/copilot/tools/handlers/context'
4646
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
47-
import { getWorkflowById, resolveWorkflowIdForUser } from '@/lib/workflows/utils'
47+
import { resolveWorkflowIdForUser } from '@/lib/workflows/utils'
4848
import {
4949
getUserEntityPermissions,
5050
isWorkspaceAccessDeniedError,
@@ -563,13 +563,7 @@ async function resolveBranch(params: {
563563
}
564564

565565
const resolvedWorkflowId = resolved.workflowId
566-
let resolvedWorkspaceId: string | undefined
567-
try {
568-
const workflow = await getWorkflowById(resolvedWorkflowId)
569-
resolvedWorkspaceId = workflow?.workspaceId ?? requestedWorkspaceId
570-
} catch {
571-
resolvedWorkspaceId = requestedWorkspaceId
572-
}
566+
const resolvedWorkspaceId = resolved.workspaceId
573567

574568
const selectedModel = model || DEFAULT_MODEL
575569
return {
@@ -1015,7 +1009,7 @@ export async function handleUnifiedChatPost(req: NextRequest) {
10151009
orchestrateOptions: {
10161010
userId: authenticatedUserId,
10171011
...(branch.kind === 'workflow' ? { workflowId: branch.workflowId } : {}),
1018-
...(branch.kind === 'workspace' ? { workspaceId: branch.workspaceId } : {}),
1012+
...(workspaceId ? { workspaceId } : {}),
10191013
chatId: actualChatId,
10201014
executionId,
10211015
runId,

apps/sim/lib/copilot/request/lifecycle/run.test.ts

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,92 @@ describe('runCopilotLifecycle', () => {
343343
)
344344
})
345345

346+
it('normalizes the initial request body with workspaceId from lifecycle options', async () => {
347+
let requestBody: Record<string, unknown> | undefined
348+
mockGetEffectiveDecryptedEnv.mockResolvedValueOnce({})
349+
mockRunStreamLoop.mockImplementationOnce(
350+
async (_fetchUrl: string, fetchOptions: RequestInit): Promise<void> => {
351+
requestBody = JSON.parse(String(fetchOptions.body))
352+
}
353+
)
354+
355+
await runCopilotLifecycle(
356+
{ message: 'hello', messageId: 'stream-1' },
357+
{
358+
userId: 'user-1',
359+
workspaceId: 'ws-1',
360+
chatId: 'chat-1',
361+
}
362+
)
363+
364+
expect(requestBody).toEqual(
365+
expect.objectContaining({
366+
workspaceId: 'ws-1',
367+
})
368+
)
369+
})
370+
371+
it('uses the lifecycle workspaceId for async tool resume requests', async () => {
372+
const requestBodies: Record<string, unknown>[] = []
373+
const fetchUrls: string[] = []
374+
const executionContext: ExecutionContext = {
375+
userId: 'user-1',
376+
workflowId: 'workflow-1',
377+
workspaceId: 'ws-1',
378+
chatId: 'chat-1',
379+
decryptedEnvVars: {},
380+
}
381+
382+
mockRunStreamLoop.mockImplementationOnce(
383+
async (
384+
fetchUrl: string,
385+
fetchOptions: RequestInit,
386+
context: StreamingContext
387+
): Promise<void> => {
388+
fetchUrls.push(fetchUrl)
389+
requestBodies.push(JSON.parse(String(fetchOptions.body)))
390+
context.toolCalls.set('tool-1', {
391+
id: 'tool-1',
392+
name: 'read',
393+
status: MothershipStreamV1ToolOutcome.success,
394+
result: { success: true, output: { content: 'file contents' } },
395+
})
396+
context.awaitingAsyncContinuation = {
397+
checkpointId: 'ckpt-1',
398+
pendingToolCallIds: ['tool-1'],
399+
}
400+
}
401+
)
402+
mockRunStreamLoop.mockImplementationOnce(
403+
async (fetchUrl: string, fetchOptions: RequestInit): Promise<void> => {
404+
fetchUrls.push(fetchUrl)
405+
requestBodies.push(JSON.parse(String(fetchOptions.body)))
406+
}
407+
)
408+
409+
await runCopilotLifecycle(
410+
{ message: 'hello', messageId: 'stream-1' },
411+
{
412+
userId: 'user-1',
413+
workspaceId: 'ws-1',
414+
workflowId: 'workflow-1',
415+
chatId: 'chat-1',
416+
executionId: 'exec-1',
417+
runId: 'run-1',
418+
executionContext,
419+
}
420+
)
421+
422+
expect(fetchUrls[1]).toBe('http://mothership.test/api/tools/resume')
423+
expect(requestBodies[1]).toEqual(
424+
expect.objectContaining({
425+
checkpointId: 'ckpt-1',
426+
userId: 'user-1',
427+
workspaceId: 'ws-1',
428+
})
429+
)
430+
})
431+
346432
it('finalizes as success when a resume fails with a retryable error then the retry succeeds', async () => {
347433
const executionContext: ExecutionContext = {
348434
userId: 'user-1',

apps/sim/lib/copilot/request/lifecycle/run.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,12 @@ const logger = createLogger('CopilotLifecycle')
4444
const MAX_RESUME_ATTEMPTS = 3
4545
const RESUME_BACKOFF_MS = [250, 500, 1000] as const
4646

47+
function nonBlankString(value: unknown): string | undefined {
48+
if (typeof value !== 'string') return undefined
49+
const trimmed = value.trim()
50+
return trimmed.length > 0 ? trimmed : undefined
51+
}
52+
4753
function resultContent(context: StreamingContext, options: CopilotLifecycleOptions): string {
4854
if (options.interactive === false && context.sawMainToolCall) {
4955
return context.finalAssistantContent
@@ -220,11 +226,21 @@ async function runCheckpointLoop(
220226
let resumeAttempt = 0
221227
const callerOnEvent = options.onEvent
222228
const mothershipBaseURL = await getMothershipBaseURL({ userId: options.userId })
229+
const lifecycleWorkspaceId = nonBlankString(options.workspaceId)
230+
231+
// Go's auth middleware re-validates every Sim -> Go request by reading
232+
// workspaceId from the JSON body and forwarding it to Sim's validate route,
233+
// where it is required for the per-member usage gate. Normalize the initial
234+
// leg from the lifecycle option so callers that only set the option (not the
235+
// raw payload) still send it on the first request.
236+
if (lifecycleWorkspaceId && !nonBlankString(payload.workspaceId)) {
237+
payload = { ...payload, workspaceId: lifecycleWorkspaceId }
238+
}
223239

224240
// Enterprise BYOK eligibility hint: set once on the initial mothership request
225241
// so Go only attempts a BYOK lookup for entitled workspaces. This is only a
226242
// gate — Go re-confirms entitlement authoritatively before using any key.
227-
payload = await withByokEligibilityHint(payload, route, options.workspaceId)
243+
payload = await withByokEligibilityHint(payload, route, lifecycleWorkspaceId)
228244

229245
for (;;) {
230246
context.streamComplete = false
@@ -458,7 +474,7 @@ async function runCheckpointLoop(
458474
streamId: context.messageId,
459475
checkpointId: continuation.checkpointId,
460476
userId: options.userId,
461-
...(options.workspaceId ? { workspaceId: options.workspaceId } : {}),
477+
...(lifecycleWorkspaceId ? { workspaceId: lifecycleWorkspaceId } : {}),
462478
results,
463479
}
464480

apps/sim/lib/workflows/utils.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ export type WorkflowResolutionResult =
108108
| {
109109
status: 'resolved'
110110
workflowId: string
111+
workspaceId: string
111112
workflowName?: string
112113
}
113114
| {
@@ -143,7 +144,18 @@ export async function resolveWorkflowIdForUser(
143144
}
144145
}
145146
const wf = await getWorkflowById(workflowId)
146-
return { status: 'resolved', workflowId, workflowName: wf?.name || undefined }
147+
if (!wf?.workspaceId) {
148+
return {
149+
status: 'not_found',
150+
message: 'No workflows found. Create a workflow first or provide a valid workflowId.',
151+
}
152+
}
153+
return {
154+
status: 'resolved',
155+
workflowId,
156+
workspaceId: wf.workspaceId,
157+
workflowName: wf.name || undefined,
158+
}
147159
}
148160

149161
const workspaceIds = await db
@@ -189,6 +201,7 @@ export async function resolveWorkflowIdForUser(
189201
return {
190202
status: 'resolved',
191203
workflowId: match.id,
204+
workspaceId: match.workspaceId,
192205
workflowName: match.name || undefined,
193206
}
194207
}
@@ -213,6 +226,7 @@ export async function resolveWorkflowIdForUser(
213226
return {
214227
status: 'resolved',
215228
workflowId: workflows[0].id,
229+
workspaceId: workflows[0].workspaceId,
216230
workflowName: workflows[0].name || undefined,
217231
}
218232
}

0 commit comments

Comments
 (0)