From 6af732a7fabcdc3edf18cf8e14b1cb4d250d95fa Mon Sep 17 00:00:00 2001 From: Siddharth Ganesan Date: Sun, 25 Jan 2026 11:46:41 -0800 Subject: [PATCH] Fix v1 --- .../tools/server/workflow/edit-workflow.ts | 121 +++++++++++++++++- 1 file changed, 120 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 1e112d3fc..acffe6db8 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,105 @@ async function validateWorkflowSelectorIds( return errors } +/** + * Pre-validates oauth-input (credential) values in operations before they are applied. + * Removes invalid credential inputs from operations so they are never applied. + * Returns validation errors for any removed credentials. + */ +async function preValidateCredentialInputs( + operations: EditWorkflowOperation[], + context: { userId: string } +): Promise<{ filteredOperations: EditWorkflowOperation[]; errors: ValidationError[] }> { + const logger = createLogger('PreValidateCredentials') + const errors: ValidationError[] = [] + + // Collect all credential values from operations that need validation + const credentialInputs: Array<{ + operationIndex: number + blockId: string + blockType: string + fieldName: string + value: string + }> = [] + + 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 in this block type + 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, + }) + } + }) + + if (credentialInputs.length === 0) { + return { filteredOperations: operations, errors } + } + + logger.info('Pre-validating credential inputs', { + credentialCount: credentialInputs.length, + userId: context.userId, + }) + + // Validate all credential IDs at once + 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) { + return { filteredOperations: operations, errors } + } + + // Deep clone operations so we can modify them + const filteredOperations = JSON.parse(JSON.stringify(operations)) as EditWorkflowOperation[] + + // Remove invalid credential inputs from operations + for (const credInput of credentialInputs) { + if (!invalidSet.has(credInput.value)) continue + + // Remove this credential input from the operation + 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, + }) + } + + // Add error for LLM feedback + 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 from operations', { + invalidCount: invalidSet.size, + errors: errors.map((e) => ({ blockId: e.blockId, field: e.field })), + }) + + return { filteredOperations, errors } +} + async function getCurrentWorkflowStateFromDb( workflowId: string ): Promise<{ workflowState: any; subBlockValues: Record> }> { @@ -2657,12 +2760,28 @@ export const editWorkflowServerTool: BaseServerTool = { // Get permission config for the user const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null + // Pre-validate credential inputs before applying operations + // This filters out invalid credentials so they never get applied + 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