Skip to content

Commit ebfdb9c

Browse files
committed
V1
1 parent 07ba174 commit ebfdb9c

File tree

4 files changed

+189
-161
lines changed

4 files changed

+189
-161
lines changed

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

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { getUserEntityPermissions } from '@/lib/permissions/utils'
77
import { saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
88
import { db } from '@/db'
99
import { workflow } from '@/db/schema'
10+
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation'
1011

1112
const logger = createLogger('WorkflowStateAPI')
1213

@@ -168,11 +169,14 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
168169
return NextResponse.json({ error: 'Access denied' }, { status: 403 })
169170
}
170171

172+
// Sanitize custom tools in agent blocks before saving
173+
const { blocks: sanitizedBlocks, warnings } = sanitizeAgentToolsInBlocks(state.blocks as any)
174+
171175
// Save to normalized tables
172176
// Ensure all required fields are present for WorkflowState type
173177
// Filter out blocks without type or name before saving
174-
const filteredBlocks = Object.entries(state.blocks).reduce(
175-
(acc, [blockId, block]) => {
178+
const filteredBlocks = Object.entries(sanitizedBlocks).reduce(
179+
(acc, [blockId, block]: [string, any]) => {
176180
if (block.type && block.name) {
177181
// Ensure all required fields are present
178182
acc[blockId] = {
@@ -184,7 +188,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
184188
height: block.height !== undefined ? block.height : 0,
185189
subBlocks: block.subBlocks || {},
186190
outputs: block.outputs || {},
187-
data: block.data || {},
188191
}
189192
}
190193
return acc
@@ -227,29 +230,20 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
227230
logger.info(`[${requestId}] Successfully saved workflow ${workflowId} state in ${elapsed}ms`)
228231

229232
return NextResponse.json(
230-
{
231-
success: true,
232-
blocksCount: Object.keys(filteredBlocks).length,
233-
edgesCount: state.edges.length,
234-
},
233+
{ success: true, warnings },
235234
{ status: 200 }
236235
)
237-
} catch (error: unknown) {
236+
} catch (error: any) {
238237
const elapsed = Date.now() - startTime
239-
if (error instanceof z.ZodError) {
240-
logger.warn(`[${requestId}] Invalid workflow state data for ${workflowId}`, {
241-
errors: error.errors,
242-
})
243-
return NextResponse.json(
244-
{ error: 'Invalid state data', details: error.errors },
245-
{ status: 400 }
246-
)
247-
}
248-
249238
logger.error(
250239
`[${requestId}] Error saving workflow ${workflowId} state after ${elapsed}ms`,
251240
error
252241
)
242+
243+
if (error instanceof z.ZodError) {
244+
return NextResponse.json({ error: 'Invalid request body', details: error.errors }, { status: 400 })
245+
}
246+
253247
return NextResponse.json({ error: 'Internal server error' }, { status: 500 })
254248
}
255249
}

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

Lines changed: 60 additions & 141 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,62 @@
1+
import crypto from 'crypto'
12
import { eq } from 'drizzle-orm'
23
import { type NextRequest, NextResponse } from 'next/server'
34
import { z } from 'zod'
45
import { env } from '@/lib/env'
56
import { createLogger } from '@/lib/logs/console/logger'
6-
import { getUserEntityPermissions } from '@/lib/permissions/utils'
7-
import { SIM_AGENT_API_URL_DEFAULT, simAgentClient } from '@/lib/sim-agent'
8-
import {
9-
loadWorkflowFromNormalizedTables,
10-
saveWorkflowToNormalizedTables,
11-
} from '@/lib/workflows/db-helpers'
12-
import { getUserId as getOAuthUserId } from '@/app/api/auth/oauth/utils'
13-
import { getBlock } from '@/blocks'
14-
import { getAllBlocks } from '@/blocks/registry'
15-
import type { BlockConfig } from '@/blocks/types'
16-
import { resolveOutputType } from '@/blocks/utils'
177
import { db } from '@/db'
18-
import { workflowCheckpoints, workflow as workflowTable } from '@/db/schema'
8+
import { workflow as workflowTable, workflowCheckpoints } from '@/db/schema'
9+
import { getAllBlocks, getBlock } from '@/blocks'
10+
import type { BlockConfig } from '@/blocks/types'
1911
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
12+
import { resolveOutputType } from '@/blocks/utils'
13+
import { getUserId } from '@/app/api/auth/oauth/utils'
14+
import { simAgentClient } from '@/lib/sim-agent'
15+
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation'
16+
import { loadWorkflowFromNormalizedTables, saveWorkflowToNormalizedTables } from '@/lib/workflows/db-helpers'
2017

21-
const SIM_AGENT_API_URL = env.SIM_AGENT_API_URL || SIM_AGENT_API_URL_DEFAULT
22-
23-
export const dynamic = 'force-dynamic'
24-
25-
const logger = createLogger('WorkflowYamlAPI')
18+
const logger = createLogger('YamlWorkflowAPI')
2619

2720
const YamlWorkflowRequestSchema = z.object({
2821
yamlContent: z.string().min(1, 'YAML content is required'),
2922
description: z.string().optional(),
30-
chatId: z.string().optional(), // For copilot checkpoints
31-
source: z.enum(['copilot', 'import', 'editor']).default('editor'),
32-
applyAutoLayout: z.boolean().default(true),
33-
createCheckpoint: z.boolean().default(false),
23+
chatId: z.string().optional(),
24+
source: z.enum(['copilot', 'editor', 'import']).default('editor'),
25+
applyAutoLayout: z.boolean().optional().default(false),
26+
createCheckpoint: z.boolean().optional().default(false),
3427
})
3528

36-
type YamlWorkflowRequest = z.infer<typeof YamlWorkflowRequestSchema>
29+
function updateBlockReferences(
30+
value: any,
31+
blockIdMapping: Map<string, string>,
32+
requestId: string
33+
): any {
34+
if (typeof value === 'string') {
35+
// Replace references in string values
36+
for (const [oldId, newId] of blockIdMapping.entries()) {
37+
if (value.includes(oldId)) {
38+
value = value
39+
.replaceAll(`<${oldId}.`, `<${newId}.`)
40+
.replaceAll(`%${oldId}.`, `%${newId}.`)
41+
}
42+
}
43+
return value
44+
}
45+
46+
if (Array.isArray(value)) {
47+
return value.map((item) => updateBlockReferences(item, blockIdMapping, requestId))
48+
}
49+
50+
if (value && typeof value === 'object') {
51+
const result: Record<string, any> = {}
52+
for (const [key, val] of Object.entries(value)) {
53+
result[key] = updateBlockReferences(val, blockIdMapping, requestId)
54+
}
55+
return result
56+
}
57+
58+
return value
59+
}
3760

3861
/**
3962
* Helper function to create a checkpoint before workflow changes
@@ -68,7 +91,7 @@ async function createWorkflowCheckpoint(
6891
{} as Record<string, BlockConfig>
6992
)
7093

71-
const generateResponse = await fetch(`${SIM_AGENT_API_URL}/api/workflow/to-yaml`, {
94+
const generateResponse = await fetch(`${env.SIM_AGENT_API_URL}/api/workflow/to-yaml`, {
7295
method: 'POST',
7396
headers: {
7497
'Content-Type': 'application/json',
@@ -114,120 +137,6 @@ async function createWorkflowCheckpoint(
114137
}
115138
}
116139

117-
/**
118-
* Helper function to get user ID with proper authentication for both tool calls and direct requests
119-
*/
120-
async function getUserId(requestId: string, workflowId: string): Promise<string | null> {
121-
// Use the OAuth utils function that handles both session and workflow-based auth
122-
const userId = await getOAuthUserId(requestId, workflowId)
123-
124-
if (!userId) {
125-
logger.warn(`[${requestId}] Could not determine user ID for workflow ${workflowId}`)
126-
return null
127-
}
128-
129-
// For additional security, verify the user has permission to access this workflow
130-
const workflowData = await db
131-
.select()
132-
.from(workflowTable)
133-
.where(eq(workflowTable.id, workflowId))
134-
.then((rows) => rows[0])
135-
136-
if (!workflowData) {
137-
logger.warn(`[${requestId}] Workflow ${workflowId} not found`)
138-
return null
139-
}
140-
141-
// Check if user has permission to update this workflow
142-
let canUpdate = false
143-
144-
// Case 1: User owns the workflow
145-
if (workflowData.userId === userId) {
146-
canUpdate = true
147-
}
148-
149-
// Case 2: Workflow belongs to a workspace and user has write or admin permission
150-
if (!canUpdate && workflowData.workspaceId) {
151-
try {
152-
const userPermission = await getUserEntityPermissions(
153-
userId,
154-
'workspace',
155-
workflowData.workspaceId
156-
)
157-
if (userPermission === 'write' || userPermission === 'admin') {
158-
canUpdate = true
159-
}
160-
} catch (error) {
161-
logger.warn(`[${requestId}] Error checking workspace permissions:`, error)
162-
}
163-
}
164-
165-
if (!canUpdate) {
166-
logger.warn(`[${requestId}] User ${userId} denied permission to update workflow ${workflowId}`)
167-
return null
168-
}
169-
170-
return userId
171-
}
172-
173-
/**
174-
* Helper function to update block references in values with new mapped IDs
175-
*/
176-
function updateBlockReferences(
177-
value: any,
178-
blockIdMapping: Map<string, string>,
179-
requestId: string
180-
): any {
181-
if (typeof value === 'string' && value.includes('<') && value.includes('>')) {
182-
let processedValue = value
183-
const blockMatches = value.match(/<([^>]+)>/g)
184-
185-
if (blockMatches) {
186-
for (const match of blockMatches) {
187-
const path = match.slice(1, -1)
188-
const [blockRef] = path.split('.')
189-
190-
// Skip system references (start, loop, parallel, variable)
191-
if (['start', 'loop', 'parallel', 'variable'].includes(blockRef.toLowerCase())) {
192-
continue
193-
}
194-
195-
// Check if this references an old block ID that needs mapping
196-
const newMappedId = blockIdMapping.get(blockRef)
197-
if (newMappedId) {
198-
logger.info(`[${requestId}] Updating block reference: ${blockRef} -> ${newMappedId}`)
199-
processedValue = processedValue.replace(
200-
new RegExp(`<${blockRef}\\.`, 'g'),
201-
`<${newMappedId}.`
202-
)
203-
processedValue = processedValue.replace(
204-
new RegExp(`<${blockRef}>`, 'g'),
205-
`<${newMappedId}>`
206-
)
207-
}
208-
}
209-
}
210-
211-
return processedValue
212-
}
213-
214-
// Handle arrays
215-
if (Array.isArray(value)) {
216-
return value.map((item) => updateBlockReferences(item, blockIdMapping, requestId))
217-
}
218-
219-
// Handle objects
220-
if (value !== null && typeof value === 'object') {
221-
const result = { ...value }
222-
for (const key in result) {
223-
result[key] = updateBlockReferences(result[key], blockIdMapping, requestId)
224-
}
225-
return result
226-
}
227-
228-
return value
229-
}
230-
231140
/**
232141
* PUT /api/workflows/[id]/yaml
233142
* Consolidated YAML workflow saving endpoint
@@ -281,7 +190,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
281190
{} as Record<string, BlockConfig>
282191
)
283192

284-
const conversionResponse = await fetch(`${SIM_AGENT_API_URL}/api/yaml/to-workflow`, {
193+
const conversionResponse = await fetch(`${env.SIM_AGENT_API_URL}/api/yaml/to-workflow`, {
285194
method: 'POST',
286195
headers: {
287196
'Content-Type': 'application/json',
@@ -541,7 +450,6 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
541450
}
542451
}
543452

544-
// Debug: Log block parent-child relationships before generating loops
545453
// Generate loop and parallel configurations
546454
const loops = generateLoopBlocks(newWorkflowState.blocks)
547455
const parallels = generateParallelBlocks(newWorkflowState.blocks)
@@ -626,6 +534,17 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
626534
}
627535
}
628536

537+
// Sanitize custom tools in agent blocks before saving
538+
const { blocks: sanitizedBlocks, warnings: sanitationWarnings } = sanitizeAgentToolsInBlocks(
539+
newWorkflowState.blocks
540+
)
541+
if (sanitationWarnings.length > 0) {
542+
logger.warn(
543+
`[${requestId}] Tool sanitation produced ${sanitationWarnings.length} warning(s)`
544+
)
545+
}
546+
newWorkflowState.blocks = sanitizedBlocks
547+
629548
// Save to database
630549
const saveResult = await saveWorkflowToNormalizedTables(workflowId, newWorkflowState)
631550

@@ -635,7 +554,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
635554
success: false,
636555
message: `Database save failed: ${saveResult.error || 'Unknown error'}`,
637556
errors: [saveResult.error || 'Database save failed'],
638-
warnings,
557+
warnings: [...warnings, ...sanitationWarnings],
639558
})
640559
}
641560

@@ -687,7 +606,7 @@ export async function PUT(request: NextRequest, { params }: { params: Promise<{
687606
parallelsCount: Object.keys(parallels).length,
688607
},
689608
errors: [],
690-
warnings,
609+
warnings: [...warnings, ...sanitationWarnings],
691610
})
692611
} catch (error) {
693612
const elapsed = Date.now() - startTime

apps/sim/lib/workflows/db-helpers.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { db } from '@/db'
44
import { workflow, workflowBlocks, workflowEdges, workflowSubflows } from '@/db/schema'
55
import type { WorkflowState } from '@/stores/workflows/workflow/types'
66
import { SUBFLOW_TYPES } from '@/stores/workflows/workflow/types'
7+
import { sanitizeAgentToolsInBlocks } from '@/lib/workflows/validation'
78

89
const logger = createLogger('WorkflowDBHelpers')
910

@@ -114,6 +115,14 @@ export async function loadWorkflowFromNormalizedTables(
114115
}
115116
})
116117

118+
// Sanitize any invalid custom tools in agent blocks to prevent client crashes
119+
const { blocks: sanitizedBlocks, warnings } = sanitizeAgentToolsInBlocks(blocksMap)
120+
if (warnings.length > 0) {
121+
logger.warn(`Sanitized workflow ${workflowId} tools with ${warnings.length} warning(s)`, {
122+
warnings,
123+
})
124+
}
125+
117126
// Convert edges to the expected format
118127
const edgesArray = edges.map((edge) => ({
119128
id: edge.id,
@@ -146,7 +155,7 @@ export async function loadWorkflowFromNormalizedTables(
146155
})
147156

148157
return {
149-
blocks: blocksMap,
158+
blocks: sanitizedBlocks,
150159
edges: edgesArray,
151160
loops,
152161
parallels,

0 commit comments

Comments
 (0)