fix(copilot): fix incorrectly sanitizing json state (#2346)

* Fix

* Fix

* Remove dead code

* Fix lint
This commit is contained in:
Siddharth Ganesan
2025-12-12 17:55:29 -08:00
committed by GitHub
parent 92db054c53
commit d06d2b01e3

View File

@@ -2,7 +2,6 @@ import type { Edge } from 'reactflow'
import { sanitizeWorkflowForSharing } from '@/lib/workflows/credentials/credential-extractor'
import type { BlockState, Loop, Parallel, WorkflowState } from '@/stores/workflows/workflow/types'
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
import { TRIGGER_PERSISTED_SUBBLOCK_IDS } from '@/triggers/constants'
/**
* Sanitized workflow state for copilot (removes all UI-specific data)
@@ -65,41 +64,6 @@ export interface ExportWorkflowState {
}
}
/**
* Check if a subblock contains sensitive/secret data
*/
function isSensitiveSubBlock(key: string, subBlock: BlockState['subBlocks'][string]): boolean {
if (TRIGGER_PERSISTED_SUBBLOCK_IDS.includes(key)) {
return false
}
// Check if it's an OAuth input type
if (subBlock.type === 'oauth-input') {
return true
}
// Check if the field name suggests it contains sensitive data
const sensitivePattern = /credential|oauth|api[_-]?key|token|secret|auth|password|bearer/i
if (sensitivePattern.test(key)) {
return true
}
// Check if the value itself looks like a secret (but not environment variable references)
if (typeof subBlock.value === 'string' && subBlock.value.length > 0) {
// Don't sanitize environment variable references like {{VAR_NAME}}
if (subBlock.value.startsWith('{{') && subBlock.value.endsWith('}}')) {
return false
}
// If it matches sensitive patterns in the value, it's likely a hardcoded secret
if (sensitivePattern.test(subBlock.value)) {
return true
}
}
return false
}
/**
* Sanitize condition blocks by removing UI-specific metadata
* Returns cleaned JSON string (not parsed array)
@@ -171,9 +135,26 @@ function sanitizeTools(tools: any[]): any[] {
}
/**
* Sanitize subblocks by removing null values, secrets, and simplifying structure
* Sort object keys recursively for consistent comparison
*/
function sortKeysRecursively(item: any): any {
if (Array.isArray(item)) {
return item.map(sortKeysRecursively)
}
if (item !== null && typeof item === 'object') {
return Object.keys(item)
.sort()
.reduce((result: any, key: string) => {
result[key] = sortKeysRecursively(item[key])
return result
}, {})
}
return item
}
/**
* Sanitize subblocks by removing null values and simplifying structure
* Maps each subblock key directly to its value instead of the full object
* Note: responseFormat is kept as an object for better copilot understanding
*/
function sanitizeSubBlocks(
subBlocks: BlockState['subBlocks']
@@ -181,76 +162,35 @@ function sanitizeSubBlocks(
const sanitized: Record<string, string | number | string[][] | object> = {}
Object.entries(subBlocks).forEach(([key, subBlock]) => {
// Special handling for responseFormat - process BEFORE null check
// so we can detect when it's added/removed
// Skip null/undefined values
if (subBlock.value === null || subBlock.value === undefined) {
return
}
// Normalize responseFormat for consistent key ordering (important for training data)
if (key === 'responseFormat') {
try {
// Handle null/undefined - skip if no value
if (subBlock.value === null || subBlock.value === undefined) {
return
}
let obj = subBlock.value
// Handle string values - parse them first
// Parse JSON string if needed
if (typeof subBlock.value === 'string') {
const trimmed = subBlock.value.trim()
if (!trimmed) {
// Empty string - skip this field
return
}
obj = JSON.parse(trimmed)
}
// Handle object values - normalize keys and keep as object for copilot
// Sort keys for consistent comparison
if (obj && typeof obj === 'object') {
// Sort keys recursively for consistent comparison
const sortKeys = (item: any): any => {
if (Array.isArray(item)) {
return item.map(sortKeys)
}
if (item !== null && typeof item === 'object') {
return Object.keys(item)
.sort()
.reduce((result: any, key: string) => {
result[key] = sortKeys(item[key])
return result
}, {})
}
return item
}
// Keep as object (not stringified) for better copilot understanding
const normalized = sortKeys(obj)
sanitized[key] = normalized
sanitized[key] = sortKeysRecursively(obj)
return
}
// If we get here, obj is not an object (maybe null or primitive) - skip it
return
} catch (error) {
// Invalid JSON - skip this field to avoid crashes
return
}
}
// Skip null/undefined values for other fields
if (subBlock.value === null || subBlock.value === undefined) {
return
}
// For sensitive fields, either omit or replace with placeholder
if (isSensitiveSubBlock(key, subBlock)) {
// If it's an environment variable reference, keep it
if (
typeof subBlock.value === 'string' &&
subBlock.value.startsWith('{{') &&
subBlock.value.endsWith('}}')
) {
} catch {
// Invalid JSON - pass through as-is
sanitized[key] = subBlock.value
return
}
// Otherwise omit the sensitive value entirely
return
}
// Special handling for condition-input type - clean UI metadata