Skip to content

Commit 43402fd

Browse files
committed
Fix
1 parent be2a9ef commit 43402fd

File tree

1 file changed

+172
-1
lines changed

1 file changed

+172
-1
lines changed

apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts

Lines changed: 172 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2508,6 +2508,10 @@ async function validateWorkflowSelectorIds(
25082508
for (const subBlockConfig of blockConfig.subBlocks) {
25092509
if (!SELECTOR_TYPES.has(subBlockConfig.type)) continue
25102510

2511+
// Skip oauth-input - credentials are pre-validated before edit application
2512+
// This allows existing collaborator credentials to remain untouched
2513+
if (subBlockConfig.type === 'oauth-input') continue
2514+
25112515
const subBlockValue = blockData.subBlocks?.[subBlockConfig.id]?.value
25122516
if (!subBlockValue) continue
25132517

@@ -2573,6 +2577,157 @@ async function validateWorkflowSelectorIds(
25732577
return errors
25742578
}
25752579

2580+
/**
2581+
* Pre-validates credential and apiKey inputs in operations before they are applied.
2582+
* - Validates oauth-input (credential) IDs belong to the user
2583+
* - Filters out apiKey inputs for hosted models when isHosted is true
2584+
* Returns validation errors for any removed inputs.
2585+
*/
2586+
async function preValidateCredentialInputs(
2587+
operations: EditWorkflowOperation[],
2588+
context: { userId: string }
2589+
): Promise<{ filteredOperations: EditWorkflowOperation[]; errors: ValidationError[] }> {
2590+
const { isHosted } = await import('@/lib/core/config/feature-flags')
2591+
const { getHostedModels } = await import('@/providers/utils')
2592+
2593+
const logger = createLogger('PreValidateCredentials')
2594+
const errors: ValidationError[] = []
2595+
2596+
// Collect credential and apiKey inputs that need validation/filtering
2597+
const credentialInputs: Array<{
2598+
operationIndex: number
2599+
blockId: string
2600+
blockType: string
2601+
fieldName: string
2602+
value: string
2603+
}> = []
2604+
2605+
const hostedApiKeyInputs: Array<{
2606+
operationIndex: number
2607+
blockId: string
2608+
blockType: string
2609+
model: string
2610+
}> = []
2611+
2612+
const hostedModels = isHosted ? getHostedModels() : []
2613+
const hostedModelsLower = new Set(hostedModels.map((m) => m.toLowerCase()))
2614+
2615+
operations.forEach((op, opIndex) => {
2616+
if (!op.params?.inputs || !op.params?.type) return
2617+
2618+
const blockConfig = getBlock(op.params.type)
2619+
if (!blockConfig) return
2620+
2621+
// Find oauth-input subblocks
2622+
for (const subBlockConfig of blockConfig.subBlocks) {
2623+
if (subBlockConfig.type !== 'oauth-input') continue
2624+
2625+
const inputValue = op.params.inputs[subBlockConfig.id]
2626+
if (!inputValue || typeof inputValue !== 'string' || inputValue.trim() === '') continue
2627+
2628+
credentialInputs.push({
2629+
operationIndex: opIndex,
2630+
blockId: op.block_id,
2631+
blockType: op.params.type,
2632+
fieldName: subBlockConfig.id,
2633+
value: inputValue,
2634+
})
2635+
}
2636+
2637+
// Check for apiKey inputs on hosted models
2638+
if (isHosted && op.params.inputs.apiKey) {
2639+
const modelValue = op.params.inputs.model
2640+
if (modelValue && typeof modelValue === 'string') {
2641+
if (hostedModelsLower.has(modelValue.toLowerCase())) {
2642+
hostedApiKeyInputs.push({
2643+
operationIndex: opIndex,
2644+
blockId: op.block_id,
2645+
blockType: op.params.type,
2646+
model: modelValue,
2647+
})
2648+
}
2649+
}
2650+
}
2651+
})
2652+
2653+
const hasCredentialsToValidate = credentialInputs.length > 0
2654+
const hasHostedApiKeysToFilter = hostedApiKeyInputs.length > 0
2655+
2656+
if (!hasCredentialsToValidate && !hasHostedApiKeysToFilter) {
2657+
return { filteredOperations: operations, errors }
2658+
}
2659+
2660+
// Deep clone operations so we can modify them
2661+
const filteredOperations = JSON.parse(JSON.stringify(operations)) as EditWorkflowOperation[]
2662+
2663+
// Filter out apiKey inputs for hosted models
2664+
if (hasHostedApiKeysToFilter) {
2665+
logger.info('Filtering apiKey inputs for hosted models', { count: hostedApiKeyInputs.length })
2666+
2667+
for (const apiKeyInput of hostedApiKeyInputs) {
2668+
const op = filteredOperations[apiKeyInput.operationIndex]
2669+
if (op.params?.inputs?.apiKey) {
2670+
delete op.params.inputs.apiKey
2671+
logger.info('Removed apiKey for hosted model', {
2672+
blockId: apiKeyInput.blockId,
2673+
model: apiKeyInput.model,
2674+
})
2675+
}
2676+
2677+
errors.push({
2678+
blockId: apiKeyInput.blockId,
2679+
blockType: apiKeyInput.blockType,
2680+
field: 'apiKey',
2681+
value: '[redacted]',
2682+
error: `API key not allowed for hosted model "${apiKeyInput.model}" - platform provides the key`,
2683+
})
2684+
}
2685+
}
2686+
2687+
// Validate credential inputs
2688+
if (hasCredentialsToValidate) {
2689+
logger.info('Pre-validating credential inputs', {
2690+
credentialCount: credentialInputs.length,
2691+
userId: context.userId,
2692+
})
2693+
2694+
const allCredentialIds = credentialInputs.map((c) => c.value)
2695+
const validationResult = await validateSelectorIds('oauth-input', allCredentialIds, context)
2696+
const invalidSet = new Set(validationResult.invalid)
2697+
2698+
if (invalidSet.size > 0) {
2699+
for (const credInput of credentialInputs) {
2700+
if (!invalidSet.has(credInput.value)) continue
2701+
2702+
const op = filteredOperations[credInput.operationIndex]
2703+
if (op.params?.inputs?.[credInput.fieldName]) {
2704+
delete op.params.inputs[credInput.fieldName]
2705+
logger.info('Removed invalid credential from operation', {
2706+
blockId: credInput.blockId,
2707+
field: credInput.fieldName,
2708+
invalidValue: credInput.value,
2709+
})
2710+
}
2711+
2712+
const warningInfo = validationResult.warning ? `. ${validationResult.warning}` : ''
2713+
errors.push({
2714+
blockId: credInput.blockId,
2715+
blockType: credInput.blockType,
2716+
field: credInput.fieldName,
2717+
value: credInput.value,
2718+
error: `Invalid credential ID "${credInput.value}" - credential does not exist or user doesn't have access${warningInfo}`,
2719+
})
2720+
}
2721+
2722+
logger.warn('Filtered out invalid credentials', {
2723+
invalidCount: invalidSet.size,
2724+
})
2725+
}
2726+
}
2727+
2728+
return { filteredOperations, errors }
2729+
}
2730+
25762731
async function getCurrentWorkflowStateFromDb(
25772732
workflowId: string
25782733
): Promise<{ workflowState: any; subBlockValues: Record<string, Record<string, any>> }> {
@@ -2657,12 +2812,28 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
26572812
// Get permission config for the user
26582813
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
26592814

2815+
// Pre-validate credential and apiKey inputs before applying operations
2816+
// This filters out invalid credentials and apiKeys for hosted models
2817+
let operationsToApply = operations
2818+
const credentialErrors: ValidationError[] = []
2819+
if (context?.userId) {
2820+
const { filteredOperations, errors: credErrors } = await preValidateCredentialInputs(
2821+
operations,
2822+
{ userId: context.userId }
2823+
)
2824+
operationsToApply = filteredOperations
2825+
credentialErrors.push(...credErrors)
2826+
}
2827+
26602828
// Apply operations directly to the workflow state
26612829
const {
26622830
state: modifiedWorkflowState,
26632831
validationErrors,
26642832
skippedItems,
2665-
} = applyOperationsToWorkflowState(workflowState, operations, permissionConfig)
2833+
} = applyOperationsToWorkflowState(workflowState, operationsToApply, permissionConfig)
2834+
2835+
// Add credential validation errors
2836+
validationErrors.push(...credentialErrors)
26662837

26672838
// Get workspaceId for selector validation
26682839
let workspaceId: string | undefined

0 commit comments

Comments
 (0)