Skip to content

Commit bc54274

Browse files
fix(workflows): migrate Copilot application boundary (#6455)
* fix(workflows): migrate Copilot application boundary * fix(workflows): finish delegated application migration * fix(workflows): encode VFS folder aliases * fix(workflows): close application composition gaps * fix(workflows): preserve VFS validation errors * fix(workflows): complete application boundary migration * test(workflows): format canonical binding coverage * fix(workflows): scope executor metadata reads * fix(workflows): bind executor metadata targets
1 parent 8b3b41b commit bc54274

110 files changed

Lines changed: 10070 additions & 6677 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

apps/sim/app/api/chat/manage/[id]/route.ts

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,17 +12,14 @@ import { isDev } from '@/lib/core/config/env-flags'
1212
import { encryptSecret } from '@/lib/core/security/encryption'
1313
import { getEmailDomain } from '@/lib/core/utils/urls'
1414
import { withRouteHandler } from '@/lib/core/utils/with-route-handler'
15+
import { checkNeedsRedeployment } from '@/lib/workflows/deployment-status'
1516
import {
1617
getWorkflowDeploymentSummary,
1718
performChatUndeploy,
1819
performFullDeploy,
1920
} from '@/lib/workflows/orchestration'
2021
import { checkChatAccess } from '@/app/api/chat/utils'
21-
import {
22-
checkNeedsRedeployment,
23-
createErrorResponse,
24-
createSuccessResponse,
25-
} from '@/app/api/workflows/utils'
22+
import { createErrorResponse, createSuccessResponse } from '@/app/api/workflows/utils'
2623
import {
2724
ChatDeployAuthNotAllowedError,
2825
validateChatDeployAuth,

apps/sim/app/api/v2/workflows/[id]/deploy/route.test.ts

Lines changed: 5 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ const mocks = vi.hoisted(() => ({
99
}))
1010

1111
vi.mock('@/lib/api/server/routes', () => ({
12+
createInternalSessionOrExecutorAuth: vi.fn(() => ({ kind: 'internal-workflow' })),
13+
createV2ResourceConcealmentPolicy: vi.fn(() => ({ kind: 'conceal-workflow' })),
1214
defineV2JsonRoute: mocks.defineRoute,
1315
v2ApiKeyAuth: { kind: 'v2-api-key' },
1416
v2RateLimits: { publicApi: { kind: 'public-api' } },
@@ -78,21 +80,9 @@ describe('/api/v2/workflows/[id]/deploy route definitions', () => {
7880
expect(v2DeployWorkflowContract.response.schema.parse(body)).toEqual(body)
7981
})
8082

81-
it('keeps product analytics on the v2 adapter', async () => {
82-
const result = { workflowId: 'workflow-1', workspaceId: 'workspace-1' }
83-
await Reflect.get(
84-
POST,
85-
'onSuccess'
86-
)({
87-
principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' },
88-
result,
89-
})
90-
expect(mocks.capture).toHaveBeenCalledWith(
91-
'user-1',
92-
'workflow_deployed',
93-
{ workflow_id: 'workflow-1', workspace_id: 'workspace-1' },
94-
expect.objectContaining({ groups: { workspace: 'workspace-1' } })
95-
)
83+
it('defers deploy analytics to durable activation', () => {
84+
expect(Reflect.get(POST, 'onSuccess')).toBeUndefined()
85+
expect(mocks.capture).not.toHaveBeenCalled()
9686
})
9787

9888
it('keeps undeploy on the authorized operation and declared response schema', () => {

apps/sim/app/api/v2/workflows/[id]/deploy/route.ts

Lines changed: 0 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,20 +43,6 @@ export const POST = defineV2JsonRoute({
4343
latestDeploymentAttempt: result.latestDeploymentAttempt ?? null,
4444
},
4545
}),
46-
onSuccess: ({ principal, result }) => {
47-
if (principal.kind !== 'personal_api_key') {
48-
throw new Error('Admin deployment unexpectedly admitted a workspace API key')
49-
}
50-
captureServerEvent(
51-
principal.userId,
52-
'workflow_deployed',
53-
{ workflow_id: result.workflowId, workspace_id: result.workspaceId },
54-
{
55-
groups: { workspace: result.workspaceId },
56-
setOnce: { first_workflow_deployed_at: new Date().toISOString() },
57-
}
58-
)
59-
},
6046
})
6147

6248
export const DELETE = defineV2JsonRoute({

apps/sim/app/api/v2/workflows/[id]/execute/route.test.ts

Lines changed: 55 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -236,7 +236,22 @@ describe('POST /api/v2/workflows/[id]/execute', () => {
236236
rateLimitSubscription: null,
237237
keyType: 'workspace',
238238
})
239-
dbChainMockFns.limit.mockResolvedValue([applicationContext])
239+
dbChainMockFns.limit
240+
.mockResolvedValueOnce([
241+
{
242+
workflowId: workflowRecord.id,
243+
workflow: workflowRecord,
244+
workspaceId: workflowRecord.workspaceId,
245+
},
246+
])
247+
.mockResolvedValueOnce([
248+
{
249+
id: applicationContext.workspaceId,
250+
organizationId: applicationContext.workspaceOrganizationId,
251+
allowPersonalApiKeys: applicationContext.allowPersonalApiKeys,
252+
billedAccountUserId: applicationContext.billedAccountUserId,
253+
},
254+
])
240255
mockAuthorize.mockResolvedValue({ allowed: true, workflow: workflowRecord })
241256
mockClaimExecutionId.mockImplementation(async (executionId: string) => ({
242257
key: `workflow-execution-id:${executionId}`,
@@ -420,9 +435,23 @@ describe('POST /api/v2/workflows/[id]/execute', () => {
420435
rateLimitSubscription: null,
421436
keyType: 'personal',
422437
})
423-
dbChainMockFns.limit.mockResolvedValueOnce([
424-
{ ...applicationContext, allowPersonalApiKeys: false },
425-
])
438+
dbChainMockFns.limit.mockReset()
439+
dbChainMockFns.limit
440+
.mockResolvedValueOnce([
441+
{
442+
workflowId: workflowRecord.id,
443+
workflow: workflowRecord,
444+
workspaceId: workflowRecord.workspaceId,
445+
},
446+
])
447+
.mockResolvedValueOnce([
448+
{
449+
id: applicationContext.workspaceId,
450+
organizationId: applicationContext.workspaceOrganizationId,
451+
allowPersonalApiKeys: false,
452+
billedAccountUserId: applicationContext.billedAccountUserId,
453+
},
454+
])
426455

427456
const res = await callExecute({ input: {} })
428457

@@ -487,12 +516,16 @@ describe('POST /api/v2/workflows/[id]/execute', () => {
487516
})
488517

489518
it('runs the anonymous public path sync but refuses async', async () => {
519+
dbChainMockFns.limit.mockReset()
490520
dbChainMockFns.limit.mockResolvedValueOnce([
491521
{ isPublicApi: true, isDeployed: true, userId: 'owner-1', workspaceId: 'workspace-1' },
492522
])
493523

494524
const okRes = await callPublicExecute({ input: {} })
495525
expect(okRes.status).toBe(200)
526+
expect(mockCheckPreAuthRate.mock.invocationCallOrder[0]).toBeLessThan(
527+
dbChainMockFns.select.mock.invocationCallOrder[0]
528+
)
496529
expect(mockAuthenticateV2ApiKey).not.toHaveBeenCalled()
497530
expect(mockCheckOperationRate).not.toHaveBeenCalled()
498531
expect(mockPreprocessExecution).toHaveBeenCalledWith(
@@ -506,7 +539,24 @@ describe('POST /api/v2/workflows/[id]/execute', () => {
506539
expect(asyncRes.status).toBe(400)
507540
})
508541

542+
it('rejects anonymous abuse before looking up the workflow', async () => {
543+
mockCheckPreAuthRate.mockResolvedValueOnce({
544+
allowed: false,
545+
remaining: 0,
546+
resetAt: new Date('2026-08-08T05:00:00Z'),
547+
retryAfterMs: 10_000,
548+
})
549+
550+
const response = await callPublicExecute({ input: {} })
551+
552+
expect(response.status).toBe(429)
553+
expect(dbChainMockFns.select).not.toHaveBeenCalled()
554+
expect(mockValidatePublicApiAllowed).not.toHaveBeenCalled()
555+
expect(mockAuthenticateV2ApiKey).not.toHaveBeenCalled()
556+
})
557+
509558
it('401s non-public workflows without a key', async () => {
559+
dbChainMockFns.limit.mockReset()
510560
dbChainMockFns.limit.mockResolvedValueOnce([
511561
{ isPublicApi: false, isDeployed: true, userId: 'owner-1', workspaceId: 'workspace-1' },
512562
])
@@ -532,6 +582,7 @@ describe('POST /api/v2/workflows/[id]/execute', () => {
532582
})
533583

534584
it('returns a safe error when canonical workflow lookup fails', async () => {
585+
dbChainMockFns.limit.mockReset()
535586
dbChainMockFns.limit.mockRejectedValueOnce(new Error('database connection details'))
536587

537588
const response = await callExecute({ input: {} })

apps/sim/app/api/v2/workflows/[id]/execute/route.ts

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import {
1111
} from '@/lib/api/contracts/v2/workflows'
1212
import { parseRequest } from '@/lib/api/server'
1313
import {
14-
admitV2Request,
14+
admitOptionalV2Request,
1515
V2RouteInfrastructureError,
1616
v2ApiKeyAuth,
1717
v2RateLimits,
@@ -112,14 +112,15 @@ export const POST = withRouteHandler(
112112
let isPublicApiAccess = false
113113
let apiKeyPrincipal: V2ApiKeyPrincipal | undefined
114114

115-
if (req.headers.has('x-api-key')) {
116-
const admission = await admitV2Request(
117-
req,
118-
workflowOperations.execute,
119-
v2ApiKeyAuth,
120-
v2RateLimits.publicApi
121-
)
122-
if (!admission.success) return admission.response
115+
const admission = await admitOptionalV2Request(
116+
req,
117+
workflowOperations.execute,
118+
v2ApiKeyAuth,
119+
v2RateLimits.publicApi
120+
)
121+
if (!admission.success) return admission.response
122+
123+
if (admission.auth) {
123124
apiKeyPrincipal = admission.auth.principal
124125
userId = admission.auth.rolloutUserId
125126
} else {

apps/sim/app/api/v2/workflows/[id]/rollback/route.test.ts

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,16 @@ import { describe, expect, it, vi } from 'vitest'
55

66
const mocks = vi.hoisted(() => ({
77
defineRoute: vi.fn((definition) => definition),
8-
capture: vi.fn(),
98
}))
109

1110
vi.mock('@/lib/api/server/routes', () => ({
11+
createInternalSessionOrExecutorAuth: vi.fn(() => ({ kind: 'internal-workflow' })),
12+
createV2ResourceConcealmentPolicy: vi.fn(() => ({ kind: 'conceal-workflow' })),
1213
defineV2JsonRoute: mocks.defineRoute,
1314
v2ApiKeyAuth: { kind: 'v2-api-key' },
1415
v2RateLimits: { publicApi: { kind: 'public-api' } },
1516
v2OrchestrationErrorPolicy: { kind: 'orchestration-errors' },
1617
}))
17-
vi.mock('@/lib/posthog/server', () => ({ captureServerEvent: mocks.capture }))
1818

1919
import { v2RollbackWorkflowContract } from '@/lib/api/contracts/v2/workflows'
2020
import { v2WorkflowErrorPolicies } from '@/lib/workflows/api'
@@ -75,19 +75,7 @@ describe('/api/v2/workflows/[id]/rollback route definition', () => {
7575
expect(v2RollbackWorkflowContract.response.schema.parse(body)).toEqual(body)
7676
})
7777

78-
it('keeps activation analytics on the v2 adapter', async () => {
79-
await Reflect.get(
80-
POST,
81-
'onSuccess'
82-
)({
83-
principal: { kind: 'personal_api_key', userId: 'user-1', keyId: 'key-1' },
84-
result: { workflowId: 'workflow-1', workspaceId: 'workspace-1', version: 1 },
85-
})
86-
expect(mocks.capture).toHaveBeenCalledWith(
87-
'user-1',
88-
'deployment_version_activated',
89-
{ workflow_id: 'workflow-1', workspace_id: 'workspace-1', version: 1 },
90-
{ groups: { workspace: 'workspace-1' } }
91-
)
78+
it('defers activation analytics to durable activation', () => {
79+
expect(Reflect.get(POST, 'onSuccess')).toBeUndefined()
9280
})
9381
})

apps/sim/app/api/v2/workflows/[id]/rollback/route.ts

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { v2RollbackWorkflowContract } from '@/lib/api/contracts/v2/workflows'
22
import { defineV2JsonRoute, v2ApiKeyAuth, v2RateLimits } from '@/lib/api/server/routes'
33
import { generateRequestId } from '@/lib/core/utils/request'
4-
import { captureServerEvent } from '@/lib/posthog/server'
54
import { v2WorkflowErrorPolicies } from '@/lib/workflows/api'
65
import { activateWorkflowVersion } from '@/lib/workflows/application/deployments'
76
import { workflowOperations } from '@/lib/workflows/application/operations'
@@ -40,19 +39,4 @@ export const POST = defineV2JsonRoute({
4039
latestDeploymentAttempt: result.latestDeploymentAttempt ?? null,
4140
},
4241
}),
43-
onSuccess: ({ principal, result }) => {
44-
if (principal.kind !== 'personal_api_key') {
45-
throw new Error('Admin activation unexpectedly admitted a workspace API key')
46-
}
47-
captureServerEvent(
48-
principal.userId,
49-
'deployment_version_activated',
50-
{
51-
workflow_id: result.workflowId,
52-
workspace_id: result.workspaceId,
53-
version: result.version,
54-
},
55-
{ groups: { workspace: result.workspaceId } }
56-
)
57-
},
5842
})

0 commit comments

Comments
 (0)