From 43402fde1ca633403bf39a9dbcd0bc137d32dd4f Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 25 Jan 2026 13:15:07 -0800 Subject: [PATCH 1/2] Fix --- .../tools/server/workflow/edit-workflow.ts | 173 +++++++++++++++++- 1 file changed, 172 insertions(+), 1 deletion(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 1e112d3fcf..3cacf4e872 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -2508,6 +2508,10 @@ async function validateWorkflowSelectorIds( for (const subBlockConfig of blockConfig.subBlocks) { if (!SELECTOR_TYPES.has(subBlockConfig.type)) continue + // Skip oauth-input - credentials are pre-validated before edit application + // This allows existing collaborator credentials to remain untouched + if (subBlockConfig.type === 'oauth-input') continue + const subBlockValue = blockData.subBlocks?.[subBlockConfig.id]?.value if (!subBlockValue) continue @@ -2573,6 +2577,157 @@ async function validateWorkflowSelectorIds( return errors } +/** + * Pre-validates credential and apiKey inputs in operations before they are applied. + * - Validates oauth-input (credential) IDs belong to the user + * - Filters out apiKey inputs for hosted models when isHosted is true + * Returns validation errors for any removed inputs. + */ +async function preValidateCredentialInputs( + operations: EditWorkflowOperation[], + context: { userId: string } +): Promise<{ filteredOperations: EditWorkflowOperation[]; errors: ValidationError[] }> { + const { isHosted } = await import('@/lib/core/config/feature-flags') + const { getHostedModels } = await import('@/providers/utils') + + const logger = createLogger('PreValidateCredentials') + const errors: ValidationError[] = [] + + // Collect credential and apiKey inputs that need validation/filtering + const credentialInputs: Array<{ + operationIndex: number + blockId: string + blockType: string + fieldName: string + value: string + }> = [] + + const hostedApiKeyInputs: Array<{ + operationIndex: number + blockId: string + blockType: string + model: string + }> = [] + + const hostedModels = isHosted ? getHostedModels() : [] + const hostedModelsLower = new Set(hostedModels.map((m) => m.toLowerCase())) + + operations.forEach((op, opIndex) => { + if (!op.params?.inputs || !op.params?.type) return + + const blockConfig = getBlock(op.params.type) + if (!blockConfig) return + + // Find oauth-input subblocks + for (const subBlockConfig of blockConfig.subBlocks) { + if (subBlockConfig.type !== 'oauth-input') continue + + const inputValue = op.params.inputs[subBlockConfig.id] + if (!inputValue || typeof inputValue !== 'string' || inputValue.trim() === '') continue + + credentialInputs.push({ + operationIndex: opIndex, + blockId: op.block_id, + blockType: op.params.type, + fieldName: subBlockConfig.id, + value: inputValue, + }) + } + + // Check for apiKey inputs on hosted models + if (isHosted && op.params.inputs.apiKey) { + const modelValue = op.params.inputs.model + if (modelValue && typeof modelValue === 'string') { + if (hostedModelsLower.has(modelValue.toLowerCase())) { + hostedApiKeyInputs.push({ + operationIndex: opIndex, + blockId: op.block_id, + blockType: op.params.type, + model: modelValue, + }) + } + } + } + }) + + const hasCredentialsToValidate = credentialInputs.length > 0 + const hasHostedApiKeysToFilter = hostedApiKeyInputs.length > 0 + + if (!hasCredentialsToValidate && !hasHostedApiKeysToFilter) { + return { filteredOperations: operations, errors } + } + + // Deep clone operations so we can modify them + const filteredOperations = JSON.parse(JSON.stringify(operations)) as EditWorkflowOperation[] + + // Filter out apiKey inputs for hosted models + if (hasHostedApiKeysToFilter) { + logger.info('Filtering apiKey inputs for hosted models', { count: hostedApiKeyInputs.length }) + + for (const apiKeyInput of hostedApiKeyInputs) { + const op = filteredOperations[apiKeyInput.operationIndex] + if (op.params?.inputs?.apiKey) { + delete op.params.inputs.apiKey + logger.info('Removed apiKey for hosted model', { + blockId: apiKeyInput.blockId, + model: apiKeyInput.model, + }) + } + + errors.push({ + blockId: apiKeyInput.blockId, + blockType: apiKeyInput.blockType, + field: 'apiKey', + value: '[redacted]', + error: `API key not allowed for hosted model "${apiKeyInput.model}" - platform provides the key`, + }) + } + } + + // Validate credential inputs + if (hasCredentialsToValidate) { + logger.info('Pre-validating credential inputs', { + credentialCount: credentialInputs.length, + userId: context.userId, + }) + + const allCredentialIds = credentialInputs.map((c) => c.value) + const validationResult = await validateSelectorIds('oauth-input', allCredentialIds, context) + const invalidSet = new Set(validationResult.invalid) + + if (invalidSet.size > 0) { + for (const credInput of credentialInputs) { + if (!invalidSet.has(credInput.value)) continue + + const op = filteredOperations[credInput.operationIndex] + if (op.params?.inputs?.[credInput.fieldName]) { + delete op.params.inputs[credInput.fieldName] + logger.info('Removed invalid credential from operation', { + blockId: credInput.blockId, + field: credInput.fieldName, + invalidValue: credInput.value, + }) + } + + const warningInfo = validationResult.warning ? `. ${validationResult.warning}` : '' + errors.push({ + blockId: credInput.blockId, + blockType: credInput.blockType, + field: credInput.fieldName, + value: credInput.value, + error: `Invalid credential ID "${credInput.value}" - credential does not exist or user doesn't have access${warningInfo}`, + }) + } + + logger.warn('Filtered out invalid credentials', { + invalidCount: invalidSet.size, + }) + } + } + + return { filteredOperations, errors } +} + async function getCurrentWorkflowStateFromDb( workflowId: string ): Promise<{ workflowState: any; subBlockValues: Record> }> { @@ -2657,12 +2812,28 @@ export const editWorkflowServerTool: BaseServerTool = { // Get permission config for the user const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + // Pre-validate credential and apiKey inputs before applying operations + // This filters out invalid credentials and apiKeys for hosted models + let operationsToApply = operations + const credentialErrors: ValidationError[] = [] + if (context?.userId) { + const { filteredOperations, errors: credErrors } = await preValidateCredentialInputs( + operations, + { userId: context.userId } + ) + operationsToApply = filteredOperations + credentialErrors.push(...credErrors) + } + // Apply operations directly to the workflow state const { state: modifiedWorkflowState, validationErrors, skippedItems, - } = applyOperationsToWorkflowState(workflowState, operations, permissionConfig) + } = applyOperationsToWorkflowState(workflowState, operationsToApply, permissionConfig) + + // Add credential validation errors + validationErrors.push(...credentialErrors) // Get workspaceId for selector validation let workspaceId: string | undefined From 3533bd009d2eb9f3c4ec8d8b1555954cbd0dff20 Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 25 Jan 2026 13:27:51 -0800 Subject: [PATCH 2/2] Fix greptile --- .../tools/server/workflow/edit-workflow.ts | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts index 3cacf4e872..491e9650a9 100644 --- a/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts +++ b/apps/sim/lib/copilot/tools/server/workflow/edit-workflow.ts @@ -2609,8 +2609,9 @@ async function preValidateCredentialInputs( model: string }> = [] - const hostedModels = isHosted ? getHostedModels() : [] - const hostedModelsLower = new Set(hostedModels.map((m) => m.toLowerCase())) + const hostedModelsLower = isHosted + ? new Set(getHostedModels().map((m) => m.toLowerCase())) + : null operations.forEach((op, opIndex) => { if (!op.params?.inputs || !op.params?.type) return @@ -2635,7 +2636,7 @@ async function preValidateCredentialInputs( } // Check for apiKey inputs on hosted models - if (isHosted && op.params.inputs.apiKey) { + if (hostedModelsLower && op.params.inputs.apiKey) { const modelValue = op.params.inputs.model if (modelValue && typeof modelValue === 'string') { if (hostedModelsLower.has(modelValue.toLowerCase())) { @@ -2658,7 +2659,7 @@ async function preValidateCredentialInputs( } // Deep clone operations so we can modify them - const filteredOperations = JSON.parse(JSON.stringify(operations)) as EditWorkflowOperation[] + const filteredOperations = structuredClone(operations) // Filter out apiKey inputs for hosted models if (hasHostedApiKeysToFilter) { @@ -2668,19 +2669,11 @@ async function preValidateCredentialInputs( const op = filteredOperations[apiKeyInput.operationIndex] if (op.params?.inputs?.apiKey) { delete op.params.inputs.apiKey - logger.info('Removed apiKey for hosted model', { + logger.debug('Silently filtered apiKey for hosted model', { blockId: apiKeyInput.blockId, model: apiKeyInput.model, }) } - - errors.push({ - blockId: apiKeyInput.blockId, - blockType: apiKeyInput.blockType, - field: 'apiKey', - value: '[redacted]', - error: `API key not allowed for hosted model "${apiKeyInput.model}" - platform provides the key`, - }) } }