@@ -2581,11 +2581,13 @@ async function validateWorkflowSelectorIds(
25812581 * Pre-validates credential and apiKey inputs in operations before they are applied.
25822582 * - Validates oauth-input (credential) IDs belong to the user
25832583 * - Filters out apiKey inputs for hosted models when isHosted is true
2584+ * - Also validates credentials and apiKeys in nestedNodes (blocks inside loop/parallel)
25842585 * Returns validation errors for any removed inputs.
25852586 */
25862587async function preValidateCredentialInputs (
25872588 operations : EditWorkflowOperation [ ] ,
2588- context : { userId : string }
2589+ context : { userId : string } ,
2590+ workflowState ?: Record < string , unknown >
25892591) : Promise < { filteredOperations : EditWorkflowOperation [ ] ; errors : ValidationError [ ] } > {
25902592 const { isHosted } = await import ( '@/lib/core/config/feature-flags' )
25912593 const { getHostedModels } = await import ( '@/providers/utils' )
@@ -2600,53 +2602,146 @@ async function preValidateCredentialInputs(
26002602 blockType : string
26012603 fieldName : string
26022604 value : string
2605+ nestedBlockId ?: string
26032606 } > = [ ]
26042607
26052608 const hostedApiKeyInputs : Array < {
26062609 operationIndex : number
26072610 blockId : string
26082611 blockType : string
26092612 model : string
2613+ nestedBlockId ?: string
26102614 } > = [ ]
26112615
26122616 const hostedModelsLower = isHosted ? new Set ( getHostedModels ( ) . map ( ( m ) => m . toLowerCase ( ) ) ) : null
26132617
2614- operations . forEach ( ( op , opIndex ) => {
2615- if ( ! op . params ?. inputs || ! op . params ?. type ) return
2616-
2617- const blockConfig = getBlock ( op . params . type )
2618+ /**
2619+ * Collect credential inputs from a block's inputs based on its block config
2620+ */
2621+ function collectCredentialInputs (
2622+ blockConfig : ReturnType < typeof getBlock > ,
2623+ inputs : Record < string , unknown > ,
2624+ opIndex : number ,
2625+ blockId : string ,
2626+ blockType : string ,
2627+ nestedBlockId ?: string
2628+ ) {
26182629 if ( ! blockConfig ) return
26192630
2620- // Find oauth-input subblocks
26212631 for ( const subBlockConfig of blockConfig . subBlocks ) {
26222632 if ( subBlockConfig . type !== 'oauth-input' ) continue
26232633
2624- const inputValue = op . params . inputs [ subBlockConfig . id ]
2634+ const inputValue = inputs [ subBlockConfig . id ]
26252635 if ( ! inputValue || typeof inputValue !== 'string' || inputValue . trim ( ) === '' ) continue
26262636
26272637 credentialInputs . push ( {
26282638 operationIndex : opIndex ,
2629- blockId : op . block_id ,
2630- blockType : op . params . type ,
2639+ blockId,
2640+ blockType,
26312641 fieldName : subBlockConfig . id ,
26322642 value : inputValue ,
2643+ nestedBlockId,
26332644 } )
26342645 }
2646+ }
26352647
2636- // Check for apiKey inputs on hosted models
2637- if ( hostedModelsLower && op . params . inputs . apiKey ) {
2638- const modelValue = op . params . inputs . model
2639- if ( modelValue && typeof modelValue === 'string' ) {
2640- if ( hostedModelsLower . has ( modelValue . toLowerCase ( ) ) ) {
2641- hostedApiKeyInputs . push ( {
2642- operationIndex : opIndex ,
2643- blockId : op . block_id ,
2644- blockType : op . params . type ,
2645- model : modelValue ,
2646- } )
2648+ /**
2649+ * Check if apiKey should be filtered for a block with the given model
2650+ */
2651+ function collectHostedApiKeyInput (
2652+ inputs : Record < string , unknown > ,
2653+ modelValue : string | undefined ,
2654+ opIndex : number ,
2655+ blockId : string ,
2656+ blockType : string ,
2657+ nestedBlockId ?: string
2658+ ) {
2659+ if ( ! hostedModelsLower || ! inputs . apiKey ) return
2660+ if ( ! modelValue || typeof modelValue !== 'string' ) return
2661+
2662+ if ( hostedModelsLower . has ( modelValue . toLowerCase ( ) ) ) {
2663+ hostedApiKeyInputs . push ( {
2664+ operationIndex : opIndex ,
2665+ blockId,
2666+ blockType,
2667+ model : modelValue ,
2668+ nestedBlockId,
2669+ } )
2670+ }
2671+ }
2672+
2673+ operations . forEach ( ( op , opIndex ) => {
2674+ // Process main block inputs
2675+ if ( op . params ?. inputs && op . params ?. type ) {
2676+ const blockConfig = getBlock ( op . params . type )
2677+ if ( blockConfig ) {
2678+ // Collect credentials from main block
2679+ collectCredentialInputs (
2680+ blockConfig ,
2681+ op . params . inputs as Record < string , unknown > ,
2682+ opIndex ,
2683+ op . block_id ,
2684+ op . params . type
2685+ )
2686+
2687+ // Check for apiKey inputs on hosted models
2688+ let modelValue = ( op . params . inputs as Record < string , unknown > ) . model as string | undefined
2689+
2690+ // For edit operations, if model is not being changed, check existing block's model
2691+ if (
2692+ ! modelValue &&
2693+ op . operation_type === 'edit' &&
2694+ ( op . params . inputs as Record < string , unknown > ) . apiKey &&
2695+ workflowState
2696+ ) {
2697+ const existingBlock = ( workflowState . blocks as Record < string , unknown > ) ?. [ op . block_id ] as
2698+ | Record < string , unknown >
2699+ | undefined
2700+ const existingSubBlocks = existingBlock ?. subBlocks as Record < string , unknown > | undefined
2701+ const existingModelSubBlock = existingSubBlocks ?. model as
2702+ | Record < string , unknown >
2703+ | undefined
2704+ modelValue = existingModelSubBlock ?. value as string | undefined
26472705 }
2706+
2707+ collectHostedApiKeyInput (
2708+ op . params . inputs as Record < string , unknown > ,
2709+ modelValue ,
2710+ opIndex ,
2711+ op . block_id ,
2712+ op . params . type
2713+ )
26482714 }
26492715 }
2716+
2717+ // Process nested nodes (blocks inside loop/parallel containers)
2718+ const nestedNodes = op . params ?. nestedNodes as
2719+ | Record < string , Record < string , unknown > >
2720+ | undefined
2721+ if ( nestedNodes ) {
2722+ Object . entries ( nestedNodes ) . forEach ( ( [ childId , childBlock ] ) => {
2723+ const childType = childBlock . type as string | undefined
2724+ const childInputs = childBlock . inputs as Record < string , unknown > | undefined
2725+ if ( ! childType || ! childInputs ) return
2726+
2727+ const childBlockConfig = getBlock ( childType )
2728+ if ( ! childBlockConfig ) return
2729+
2730+ // Collect credentials from nested block
2731+ collectCredentialInputs (
2732+ childBlockConfig ,
2733+ childInputs ,
2734+ opIndex ,
2735+ op . block_id ,
2736+ childType ,
2737+ childId
2738+ )
2739+
2740+ // Check for apiKey inputs on hosted models in nested block
2741+ const modelValue = childInputs . model as string | undefined
2742+ collectHostedApiKeyInput ( childInputs , modelValue , opIndex , op . block_id , childType , childId )
2743+ } )
2744+ }
26502745 } )
26512746
26522747 const hasCredentialsToValidate = credentialInputs . length > 0
@@ -2665,7 +2760,32 @@ async function preValidateCredentialInputs(
26652760
26662761 for ( const apiKeyInput of hostedApiKeyInputs ) {
26672762 const op = filteredOperations [ apiKeyInput . operationIndex ]
2668- if ( op . params ?. inputs ?. apiKey ) {
2763+
2764+ // Handle nested block apiKey filtering
2765+ if ( apiKeyInput . nestedBlockId ) {
2766+ const nestedNodes = op . params ?. nestedNodes as
2767+ | Record < string , Record < string , unknown > >
2768+ | undefined
2769+ const nestedBlock = nestedNodes ?. [ apiKeyInput . nestedBlockId ]
2770+ const nestedInputs = nestedBlock ?. inputs as Record < string , unknown > | undefined
2771+ if ( nestedInputs ?. apiKey ) {
2772+ nestedInputs . apiKey = undefined
2773+ logger . debug ( 'Filtered apiKey for hosted model in nested block' , {
2774+ parentBlockId : apiKeyInput . blockId ,
2775+ nestedBlockId : apiKeyInput . nestedBlockId ,
2776+ model : apiKeyInput . model ,
2777+ } )
2778+
2779+ errors . push ( {
2780+ blockId : apiKeyInput . nestedBlockId ,
2781+ blockType : apiKeyInput . blockType ,
2782+ field : 'apiKey' ,
2783+ value : '[redacted]' ,
2784+ error : `Cannot set API key for hosted model "${ apiKeyInput . model } " - API keys are managed by the platform when using hosted models` ,
2785+ } )
2786+ }
2787+ } else if ( op . params ?. inputs ?. apiKey ) {
2788+ // Handle main block apiKey filtering
26692789 op . params . inputs . apiKey = undefined
26702790 logger . debug ( 'Filtered apiKey for hosted model' , {
26712791 blockId : apiKeyInput . blockId ,
@@ -2699,7 +2819,25 @@ async function preValidateCredentialInputs(
26992819 if ( ! invalidSet . has ( credInput . value ) ) continue
27002820
27012821 const op = filteredOperations [ credInput . operationIndex ]
2702- if ( op . params ?. inputs ?. [ credInput . fieldName ] ) {
2822+
2823+ // Handle nested block credential removal
2824+ if ( credInput . nestedBlockId ) {
2825+ const nestedNodes = op . params ?. nestedNodes as
2826+ | Record < string , Record < string , unknown > >
2827+ | undefined
2828+ const nestedBlock = nestedNodes ?. [ credInput . nestedBlockId ]
2829+ const nestedInputs = nestedBlock ?. inputs as Record < string , unknown > | undefined
2830+ if ( nestedInputs ?. [ credInput . fieldName ] ) {
2831+ delete nestedInputs [ credInput . fieldName ]
2832+ logger . info ( 'Removed invalid credential from nested block' , {
2833+ parentBlockId : credInput . blockId ,
2834+ nestedBlockId : credInput . nestedBlockId ,
2835+ field : credInput . fieldName ,
2836+ invalidValue : credInput . value ,
2837+ } )
2838+ }
2839+ } else if ( op . params ?. inputs ?. [ credInput . fieldName ] ) {
2840+ // Handle main block credential removal
27032841 delete op . params . inputs [ credInput . fieldName ]
27042842 logger . info ( 'Removed invalid credential from operation' , {
27052843 blockId : credInput . blockId ,
@@ -2709,8 +2847,9 @@ async function preValidateCredentialInputs(
27092847 }
27102848
27112849 const warningInfo = validationResult . warning ? `. ${ validationResult . warning } ` : ''
2850+ const errorBlockId = credInput . nestedBlockId ?? credInput . blockId
27122851 errors . push ( {
2713- blockId : credInput . blockId ,
2852+ blockId : errorBlockId ,
27142853 blockType : credInput . blockType ,
27152854 field : credInput . fieldName ,
27162855 value : credInput . value ,
@@ -2818,7 +2957,8 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
28182957 if ( context ?. userId ) {
28192958 const { filteredOperations, errors : credErrors } = await preValidateCredentialInputs (
28202959 operations ,
2821- { userId : context . userId }
2960+ { userId : context . userId } ,
2961+ workflowState
28222962 )
28232963 operationsToApply = filteredOperations
28242964 credentialErrors . push ( ...credErrors )
0 commit comments