Skip to content

Commit d06d2b0

Browse files
authored
fix(copilot): fix incorrectly sanitizing json state (#2346)
* Fix * Fix * Remove dead code * Fix lint
1 parent 92db054 commit d06d2b0

File tree

1 file changed

+31
-91
lines changed

1 file changed

+31
-91
lines changed

apps/sim/lib/workflows/sanitization/json-sanitizer.ts

Lines changed: 31 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import type { Edge } from 'reactflow'
22
import { sanitizeWorkflowForSharing } from '@/lib/workflows/credentials/credential-extractor'
33
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
44
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
5-
import { TRIGGER_PERSISTED_SUBBLOCK_IDS } from '@/triggers/constants'
65

76
/**
87
* Sanitized workflow state for copilot (removes all UI-specific data)
@@ -65,41 +64,6 @@ export interface ExportWorkflowState {
6564
}
6665
}
6766

68-
/**
69-
* Check if a subblock contains sensitive/secret data
70-
*/
71-
function isSensitiveSubBlock(key: string, subBlock: BlockState['subBlocks'][string]): boolean {
72-
if (TRIGGER_PERSISTED_SUBBLOCK_IDS.includes(key)) {
73-
return false
74-
}
75-
76-
// Check if it's an OAuth input type
77-
if (subBlock.type === 'oauth-input') {
78-
return true
79-
}
80-
81-
// Check if the field name suggests it contains sensitive data
82-
const sensitivePattern = /credential|oauth|api[_-]?key|token|secret|auth|password|bearer/i
83-
if (sensitivePattern.test(key)) {
84-
return true
85-
}
86-
87-
// Check if the value itself looks like a secret (but not environment variable references)
88-
if (typeof subBlock.value === 'string' && subBlock.value.length > 0) {
89-
// Don't sanitize environment variable references like {{VAR_NAME}}
90-
if (subBlock.value.startsWith('{{') && subBlock.value.endsWith('}}')) {
91-
return false
92-
}
93-
94-
// If it matches sensitive patterns in the value, it's likely a hardcoded secret
95-
if (sensitivePattern.test(subBlock.value)) {
96-
return true
97-
}
98-
}
99-
100-
return false
101-
}
102-
10367
/**
10468
* Sanitize condition blocks by removing UI-specific metadata
10569
* Returns cleaned JSON string (not parsed array)
@@ -171,86 +135,62 @@ function sanitizeTools(tools: any[]): any[] {
171135
}
172136

173137
/**
174-
* Sanitize subblocks by removing null values, secrets, and simplifying structure
138+
* Sort object keys recursively for consistent comparison
139+
*/
140+
function sortKeysRecursively(item: any): any {
141+
if (Array.isArray(item)) {
142+
return item.map(sortKeysRecursively)
143+
}
144+
if (item !== null && typeof item === 'object') {
145+
return Object.keys(item)
146+
.sort()
147+
.reduce((result: any, key: string) => {
148+
result[key] = sortKeysRecursively(item[key])
149+
return result
150+
}, {})
151+
}
152+
return item
153+
}
154+
155+
/**
156+
* Sanitize subblocks by removing null values and simplifying structure
175157
* Maps each subblock key directly to its value instead of the full object
176-
* Note: responseFormat is kept as an object for better copilot understanding
177158
*/
178159
function sanitizeSubBlocks(
179160
subBlocks: BlockState['subBlocks']
180161
): Record<string, string | number | string[][] | object> {
181162
const sanitized: Record<string, string | number | string[][] | object> = {}
182163

183164
Object.entries(subBlocks).forEach(([key, subBlock]) => {
184-
// Special handling for responseFormat - process BEFORE null check
185-
// so we can detect when it's added/removed
165+
// Skip null/undefined values
166+
if (subBlock.value === null || subBlock.value === undefined) {
167+
return
168+
}
169+
170+
// Normalize responseFormat for consistent key ordering (important for training data)
186171
if (key === 'responseFormat') {
187172
try {
188-
// Handle null/undefined - skip if no value
189-
if (subBlock.value === null || subBlock.value === undefined) {
190-
return
191-
}
192-
193173
let obj = subBlock.value
194174

195-
// Handle string values - parse them first
175+
// Parse JSON string if needed
196176
if (typeof subBlock.value === 'string') {
197177
const trimmed = subBlock.value.trim()
198178
if (!trimmed) {
199-
// Empty string - skip this field
200179
return
201180
}
202181
obj = JSON.parse(trimmed)
203182
}
204183

205-
// Handle object values - normalize keys and keep as object for copilot
184+
// Sort keys for consistent comparison
206185
if (obj && typeof obj === 'object') {
207-
// Sort keys recursively for consistent comparison
208-
const sortKeys = (item: any): any => {
209-
if (Array.isArray(item)) {
210-
return item.map(sortKeys)
211-
}
212-
if (item !== null && typeof item === 'object') {
213-
return Object.keys(item)
214-
.sort()
215-
.reduce((result: any, key: string) => {
216-
result[key] = sortKeys(item[key])
217-
return result
218-
}, {})
219-
}
220-
return item
221-
}
222-
223-
// Keep as object (not stringified) for better copilot understanding
224-
const normalized = sortKeys(obj)
225-
sanitized[key] = normalized
186+
sanitized[key] = sortKeysRecursively(obj)
226187
return
227188
}
228-
229-
// If we get here, obj is not an object (maybe null or primitive) - skip it
230-
return
231-
} catch (error) {
232-
// Invalid JSON - skip this field to avoid crashes
233-
return
234-
}
235-
}
236-
237-
// Skip null/undefined values for other fields
238-
if (subBlock.value === null || subBlock.value === undefined) {
239-
return
240-
}
241-
242-
// For sensitive fields, either omit or replace with placeholder
243-
if (isSensitiveSubBlock(key, subBlock)) {
244-
// If it's an environment variable reference, keep it
245-
if (
246-
typeof subBlock.value === 'string' &&
247-
subBlock.value.startsWith('{{') &&
248-
subBlock.value.endsWith('}}')
249-
) {
189+
} catch {
190+
// Invalid JSON - pass through as-is
250191
sanitized[key] = subBlock.value
192+
return
251193
}
252-
// Otherwise omit the sensitive value entirely
253-
return
254194
}
255195

256196
// Special handling for condition-input type - clean UI metadata

0 commit comments

Comments
 (0)