mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-30 01:07:59 -05:00
Compare commits
43 Commits
cursor/not
...
v0.5.76
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
028bc652c2 | ||
|
|
c6bf5cd58c | ||
|
|
11dc18a80d | ||
|
|
ab4e9dc72f | ||
|
|
1c58c35bd8 | ||
|
|
d63a5cb504 | ||
|
|
8bd5d41723 | ||
|
|
c12931bc50 | ||
|
|
e9c4251c1c | ||
|
|
cc2be33d6b | ||
|
|
45371e521e | ||
|
|
0ce0f98aa5 | ||
|
|
dff1c9d083 | ||
|
|
b09f683072 | ||
|
|
a8bb0db660 | ||
|
|
af82820a28 | ||
|
|
4372841797 | ||
|
|
5e8c843241 | ||
|
|
7bf3d73ee6 | ||
|
|
7ffc11a738 | ||
|
|
be578e2ed7 | ||
|
|
f415e5edc4 | ||
|
|
13a6e6c3fa | ||
|
|
f5ab7f21ae | ||
|
|
bfb6fffe38 | ||
|
|
4fbec0a43f | ||
|
|
585f5e365b | ||
|
|
3792bdd252 | ||
|
|
eb5d1f3e5b | ||
|
|
54ab82c8dd | ||
|
|
f895bf469b | ||
|
|
dd3209af06 | ||
|
|
b6ba3b50a7 | ||
|
|
b304233062 | ||
|
|
57e4b49bd6 | ||
|
|
e12dd204ed | ||
|
|
3d9d9cbc54 | ||
|
|
0f4ec962ad | ||
|
|
4827866f9a | ||
|
|
3e697d9ed9 | ||
|
|
4431a1a484 | ||
|
|
4d1a9a3f22 | ||
|
|
eb07a080fb |
@@ -461,14 +461,12 @@ function WorkflowPreviewBlockInner({ data }: NodeProps<WorkflowPreviewBlockData>
|
||||
className={`flex items-center justify-between p-[8px] ${hasContentBelowHeader ? 'border-[var(--border-1)] border-b' : ''}`}
|
||||
>
|
||||
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-[10px]'>
|
||||
{!isNoteBlock && (
|
||||
<div
|
||||
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
|
||||
style={{ background: enabled ? blockConfig.bgColor : 'gray' }}
|
||||
>
|
||||
<IconComponent className='h-[16px] w-[16px] text-white' />
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
|
||||
style={{ background: enabled ? blockConfig.bgColor : 'gray' }}
|
||||
>
|
||||
<IconComponent className='h-[16px] w-[16px] text-white' />
|
||||
</div>
|
||||
<span
|
||||
className={`truncate font-medium text-[16px] ${!enabled ? 'text-[var(--text-muted)]' : ''}`}
|
||||
title={name}
|
||||
|
||||
@@ -2417,177 +2417,4 @@ describe('EdgeManager', () => {
|
||||
expect(successReady).toContain(targetId)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Condition with loop downstream - deactivation propagation', () => {
|
||||
it('should deactivate nodes after loop when condition branch containing loop is deactivated', () => {
|
||||
// Scenario: condition → (if) → sentinel_start → loopBody → sentinel_end → (loop_exit) → after_loop
|
||||
// → (else) → other_branch
|
||||
// When condition takes "else" path, the entire if-branch including nodes after the loop should be deactivated
|
||||
const conditionId = 'condition'
|
||||
const sentinelStartId = 'sentinel-start'
|
||||
const loopBodyId = 'loop-body'
|
||||
const sentinelEndId = 'sentinel-end'
|
||||
const afterLoopId = 'after-loop'
|
||||
const otherBranchId = 'other-branch'
|
||||
|
||||
const conditionNode = createMockNode(conditionId, [
|
||||
{ target: sentinelStartId, sourceHandle: 'condition-if' },
|
||||
{ target: otherBranchId, sourceHandle: 'condition-else' },
|
||||
])
|
||||
|
||||
const sentinelStartNode = createMockNode(
|
||||
sentinelStartId,
|
||||
[{ target: loopBodyId }],
|
||||
[conditionId]
|
||||
)
|
||||
|
||||
const loopBodyNode = createMockNode(
|
||||
loopBodyId,
|
||||
[{ target: sentinelEndId }],
|
||||
[sentinelStartId]
|
||||
)
|
||||
|
||||
const sentinelEndNode = createMockNode(
|
||||
sentinelEndId,
|
||||
[
|
||||
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
|
||||
{ target: afterLoopId, sourceHandle: 'loop_exit' },
|
||||
],
|
||||
[loopBodyId]
|
||||
)
|
||||
|
||||
const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
|
||||
const otherBranchNode = createMockNode(otherBranchId, [], [conditionId])
|
||||
|
||||
const nodes = new Map<string, DAGNode>([
|
||||
[conditionId, conditionNode],
|
||||
[sentinelStartId, sentinelStartNode],
|
||||
[loopBodyId, loopBodyNode],
|
||||
[sentinelEndId, sentinelEndNode],
|
||||
[afterLoopId, afterLoopNode],
|
||||
[otherBranchId, otherBranchNode],
|
||||
])
|
||||
|
||||
const dag = createMockDAG(nodes)
|
||||
const edgeManager = new EdgeManager(dag)
|
||||
|
||||
// Condition selects "else" branch, deactivating the "if" branch (which contains the loop)
|
||||
const readyNodes = edgeManager.processOutgoingEdges(conditionNode, { selectedOption: 'else' })
|
||||
|
||||
// Only otherBranch should be ready
|
||||
expect(readyNodes).toContain(otherBranchId)
|
||||
expect(readyNodes).not.toContain(sentinelStartId)
|
||||
|
||||
// afterLoop should NOT be ready - its incoming edge from sentinel_end should be deactivated
|
||||
expect(readyNodes).not.toContain(afterLoopId)
|
||||
|
||||
// Verify that countActiveIncomingEdges returns 0 for afterLoop
|
||||
// (meaning the loop_exit edge was properly deactivated)
|
||||
// Note: isNodeReady returns true when all edges are deactivated (no pending deps),
|
||||
// but the node won't be in readyNodes since it wasn't reached via an active path
|
||||
expect(edgeManager.isNodeReady(afterLoopNode)).toBe(true) // All edges deactivated = no blocking deps
|
||||
})
|
||||
|
||||
it('should deactivate nodes after parallel when condition branch containing parallel is deactivated', () => {
|
||||
// Similar scenario with parallel instead of loop
|
||||
const conditionId = 'condition'
|
||||
const parallelStartId = 'parallel-start'
|
||||
const parallelBodyId = 'parallel-body'
|
||||
const parallelEndId = 'parallel-end'
|
||||
const afterParallelId = 'after-parallel'
|
||||
const otherBranchId = 'other-branch'
|
||||
|
||||
const conditionNode = createMockNode(conditionId, [
|
||||
{ target: parallelStartId, sourceHandle: 'condition-if' },
|
||||
{ target: otherBranchId, sourceHandle: 'condition-else' },
|
||||
])
|
||||
|
||||
const parallelStartNode = createMockNode(
|
||||
parallelStartId,
|
||||
[{ target: parallelBodyId }],
|
||||
[conditionId]
|
||||
)
|
||||
|
||||
const parallelBodyNode = createMockNode(
|
||||
parallelBodyId,
|
||||
[{ target: parallelEndId }],
|
||||
[parallelStartId]
|
||||
)
|
||||
|
||||
const parallelEndNode = createMockNode(
|
||||
parallelEndId,
|
||||
[{ target: afterParallelId, sourceHandle: 'parallel_exit' }],
|
||||
[parallelBodyId]
|
||||
)
|
||||
|
||||
const afterParallelNode = createMockNode(afterParallelId, [], [parallelEndId])
|
||||
const otherBranchNode = createMockNode(otherBranchId, [], [conditionId])
|
||||
|
||||
const nodes = new Map<string, DAGNode>([
|
||||
[conditionId, conditionNode],
|
||||
[parallelStartId, parallelStartNode],
|
||||
[parallelBodyId, parallelBodyNode],
|
||||
[parallelEndId, parallelEndNode],
|
||||
[afterParallelId, afterParallelNode],
|
||||
[otherBranchId, otherBranchNode],
|
||||
])
|
||||
|
||||
const dag = createMockDAG(nodes)
|
||||
const edgeManager = new EdgeManager(dag)
|
||||
|
||||
// Condition selects "else" branch
|
||||
const readyNodes = edgeManager.processOutgoingEdges(conditionNode, { selectedOption: 'else' })
|
||||
|
||||
expect(readyNodes).toContain(otherBranchId)
|
||||
expect(readyNodes).not.toContain(parallelStartId)
|
||||
expect(readyNodes).not.toContain(afterParallelId)
|
||||
// isNodeReady returns true when all edges are deactivated (no pending deps)
|
||||
expect(edgeManager.isNodeReady(afterParallelNode)).toBe(true)
|
||||
})
|
||||
|
||||
it('should still correctly handle normal loop exit (not deactivate when loop runs)', () => {
|
||||
// When a loop actually executes and exits normally, after_loop should become ready
|
||||
const sentinelStartId = 'sentinel-start'
|
||||
const loopBodyId = 'loop-body'
|
||||
const sentinelEndId = 'sentinel-end'
|
||||
const afterLoopId = 'after-loop'
|
||||
|
||||
const sentinelStartNode = createMockNode(sentinelStartId, [{ target: loopBodyId }])
|
||||
|
||||
const loopBodyNode = createMockNode(
|
||||
loopBodyId,
|
||||
[{ target: sentinelEndId }],
|
||||
[sentinelStartId]
|
||||
)
|
||||
|
||||
const sentinelEndNode = createMockNode(
|
||||
sentinelEndId,
|
||||
[
|
||||
{ target: sentinelStartId, sourceHandle: 'loop_continue' },
|
||||
{ target: afterLoopId, sourceHandle: 'loop_exit' },
|
||||
],
|
||||
[loopBodyId]
|
||||
)
|
||||
|
||||
const afterLoopNode = createMockNode(afterLoopId, [], [sentinelEndId])
|
||||
|
||||
const nodes = new Map<string, DAGNode>([
|
||||
[sentinelStartId, sentinelStartNode],
|
||||
[loopBodyId, loopBodyNode],
|
||||
[sentinelEndId, sentinelEndNode],
|
||||
[afterLoopId, afterLoopNode],
|
||||
])
|
||||
|
||||
const dag = createMockDAG(nodes)
|
||||
const edgeManager = new EdgeManager(dag)
|
||||
|
||||
// Simulate sentinel_end completing with loop_exit (loop is done)
|
||||
const readyNodes = edgeManager.processOutgoingEdges(sentinelEndNode, {
|
||||
selectedRoute: 'loop_exit',
|
||||
})
|
||||
|
||||
// afterLoop should be ready
|
||||
expect(readyNodes).toContain(afterLoopId)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -243,7 +243,7 @@ export class EdgeManager {
|
||||
}
|
||||
|
||||
for (const [, outgoingEdge] of targetNode.outgoingEdges) {
|
||||
if (!this.isBackwardsEdge(outgoingEdge.sourceHandle)) {
|
||||
if (!this.isControlEdge(outgoingEdge.sourceHandle)) {
|
||||
this.deactivateEdgeAndDescendants(
|
||||
targetId,
|
||||
outgoingEdge.target,
|
||||
|
||||
@@ -10,7 +10,6 @@ import {
|
||||
type KnowledgeBaseArgs,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { useCopilotStore } from '@/stores/panel/copilot/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
|
||||
/**
|
||||
* Client tool for knowledge base operations
|
||||
@@ -103,19 +102,7 @@ export class KnowledgeBaseClientTool extends BaseClientTool {
|
||||
const logger = createLogger('KnowledgeBaseClientTool')
|
||||
try {
|
||||
this.setState(ClientToolCallState.executing)
|
||||
|
||||
// Get the workspace ID from the workflow registry hydration state
|
||||
const { hydration } = useWorkflowRegistry.getState()
|
||||
const workspaceId = hydration.workspaceId
|
||||
|
||||
// Build payload with workspace ID included in args
|
||||
const payload: KnowledgeBaseArgs = {
|
||||
...(args || { operation: 'list' }),
|
||||
args: {
|
||||
...(args?.args || {}),
|
||||
workspaceId: workspaceId || undefined,
|
||||
},
|
||||
}
|
||||
const payload: KnowledgeBaseArgs = { ...(args || { operation: 'list' }) }
|
||||
|
||||
const res = await fetch('/api/copilot/execute-copilot-server-tool', {
|
||||
method: 'POST',
|
||||
|
||||
@@ -2508,10 +2508,6 @@ 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
|
||||
|
||||
@@ -2577,295 +2573,6 @@ 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
|
||||
* - Also validates credentials and apiKeys in nestedNodes (blocks inside loop/parallel)
|
||||
* Returns validation errors for any removed inputs.
|
||||
*/
|
||||
async function preValidateCredentialInputs(
|
||||
operations: EditWorkflowOperation[],
|
||||
context: { userId: string },
|
||||
workflowState?: Record<string, unknown>
|
||||
): 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
|
||||
nestedBlockId?: string
|
||||
}> = []
|
||||
|
||||
const hostedApiKeyInputs: Array<{
|
||||
operationIndex: number
|
||||
blockId: string
|
||||
blockType: string
|
||||
model: string
|
||||
nestedBlockId?: string
|
||||
}> = []
|
||||
|
||||
const hostedModelsLower = isHosted ? new Set(getHostedModels().map((m) => m.toLowerCase())) : null
|
||||
|
||||
/**
|
||||
* Collect credential inputs from a block's inputs based on its block config
|
||||
*/
|
||||
function collectCredentialInputs(
|
||||
blockConfig: ReturnType<typeof getBlock>,
|
||||
inputs: Record<string, unknown>,
|
||||
opIndex: number,
|
||||
blockId: string,
|
||||
blockType: string,
|
||||
nestedBlockId?: string
|
||||
) {
|
||||
if (!blockConfig) return
|
||||
|
||||
for (const subBlockConfig of blockConfig.subBlocks) {
|
||||
if (subBlockConfig.type !== 'oauth-input') continue
|
||||
|
||||
const inputValue = inputs[subBlockConfig.id]
|
||||
if (!inputValue || typeof inputValue !== 'string' || inputValue.trim() === '') continue
|
||||
|
||||
credentialInputs.push({
|
||||
operationIndex: opIndex,
|
||||
blockId,
|
||||
blockType,
|
||||
fieldName: subBlockConfig.id,
|
||||
value: inputValue,
|
||||
nestedBlockId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if apiKey should be filtered for a block with the given model
|
||||
*/
|
||||
function collectHostedApiKeyInput(
|
||||
inputs: Record<string, unknown>,
|
||||
modelValue: string | undefined,
|
||||
opIndex: number,
|
||||
blockId: string,
|
||||
blockType: string,
|
||||
nestedBlockId?: string
|
||||
) {
|
||||
if (!hostedModelsLower || !inputs.apiKey) return
|
||||
if (!modelValue || typeof modelValue !== 'string') return
|
||||
|
||||
if (hostedModelsLower.has(modelValue.toLowerCase())) {
|
||||
hostedApiKeyInputs.push({
|
||||
operationIndex: opIndex,
|
||||
blockId,
|
||||
blockType,
|
||||
model: modelValue,
|
||||
nestedBlockId,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
operations.forEach((op, opIndex) => {
|
||||
// Process main block inputs
|
||||
if (op.params?.inputs && op.params?.type) {
|
||||
const blockConfig = getBlock(op.params.type)
|
||||
if (blockConfig) {
|
||||
// Collect credentials from main block
|
||||
collectCredentialInputs(
|
||||
blockConfig,
|
||||
op.params.inputs as Record<string, unknown>,
|
||||
opIndex,
|
||||
op.block_id,
|
||||
op.params.type
|
||||
)
|
||||
|
||||
// Check for apiKey inputs on hosted models
|
||||
let modelValue = (op.params.inputs as Record<string, unknown>).model as string | undefined
|
||||
|
||||
// For edit operations, if model is not being changed, check existing block's model
|
||||
if (
|
||||
!modelValue &&
|
||||
op.operation_type === 'edit' &&
|
||||
(op.params.inputs as Record<string, unknown>).apiKey &&
|
||||
workflowState
|
||||
) {
|
||||
const existingBlock = (workflowState.blocks as Record<string, unknown>)?.[op.block_id] as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
const existingSubBlocks = existingBlock?.subBlocks as Record<string, unknown> | undefined
|
||||
const existingModelSubBlock = existingSubBlocks?.model as
|
||||
| Record<string, unknown>
|
||||
| undefined
|
||||
modelValue = existingModelSubBlock?.value as string | undefined
|
||||
}
|
||||
|
||||
collectHostedApiKeyInput(
|
||||
op.params.inputs as Record<string, unknown>,
|
||||
modelValue,
|
||||
opIndex,
|
||||
op.block_id,
|
||||
op.params.type
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Process nested nodes (blocks inside loop/parallel containers)
|
||||
const nestedNodes = op.params?.nestedNodes as
|
||||
| Record<string, Record<string, unknown>>
|
||||
| undefined
|
||||
if (nestedNodes) {
|
||||
Object.entries(nestedNodes).forEach(([childId, childBlock]) => {
|
||||
const childType = childBlock.type as string | undefined
|
||||
const childInputs = childBlock.inputs as Record<string, unknown> | undefined
|
||||
if (!childType || !childInputs) return
|
||||
|
||||
const childBlockConfig = getBlock(childType)
|
||||
if (!childBlockConfig) return
|
||||
|
||||
// Collect credentials from nested block
|
||||
collectCredentialInputs(
|
||||
childBlockConfig,
|
||||
childInputs,
|
||||
opIndex,
|
||||
op.block_id,
|
||||
childType,
|
||||
childId
|
||||
)
|
||||
|
||||
// Check for apiKey inputs on hosted models in nested block
|
||||
const modelValue = childInputs.model as string | undefined
|
||||
collectHostedApiKeyInput(childInputs, modelValue, opIndex, op.block_id, childType, childId)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
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 = structuredClone(operations)
|
||||
|
||||
// Filter out apiKey inputs for hosted models and add validation errors
|
||||
if (hasHostedApiKeysToFilter) {
|
||||
logger.info('Filtering apiKey inputs for hosted models', { count: hostedApiKeyInputs.length })
|
||||
|
||||
for (const apiKeyInput of hostedApiKeyInputs) {
|
||||
const op = filteredOperations[apiKeyInput.operationIndex]
|
||||
|
||||
// Handle nested block apiKey filtering
|
||||
if (apiKeyInput.nestedBlockId) {
|
||||
const nestedNodes = op.params?.nestedNodes as
|
||||
| Record<string, Record<string, unknown>>
|
||||
| undefined
|
||||
const nestedBlock = nestedNodes?.[apiKeyInput.nestedBlockId]
|
||||
const nestedInputs = nestedBlock?.inputs as Record<string, unknown> | undefined
|
||||
if (nestedInputs?.apiKey) {
|
||||
nestedInputs.apiKey = undefined
|
||||
logger.debug('Filtered apiKey for hosted model in nested block', {
|
||||
parentBlockId: apiKeyInput.blockId,
|
||||
nestedBlockId: apiKeyInput.nestedBlockId,
|
||||
model: apiKeyInput.model,
|
||||
})
|
||||
|
||||
errors.push({
|
||||
blockId: apiKeyInput.nestedBlockId,
|
||||
blockType: apiKeyInput.blockType,
|
||||
field: 'apiKey',
|
||||
value: '[redacted]',
|
||||
error: `Cannot set API key for hosted model "${apiKeyInput.model}" - API keys are managed by the platform when using hosted models`,
|
||||
})
|
||||
}
|
||||
} else if (op.params?.inputs?.apiKey) {
|
||||
// Handle main block apiKey filtering
|
||||
op.params.inputs.apiKey = undefined
|
||||
logger.debug('Filtered apiKey for hosted model', {
|
||||
blockId: apiKeyInput.blockId,
|
||||
model: apiKeyInput.model,
|
||||
})
|
||||
|
||||
errors.push({
|
||||
blockId: apiKeyInput.blockId,
|
||||
blockType: apiKeyInput.blockType,
|
||||
field: 'apiKey',
|
||||
value: '[redacted]',
|
||||
error: `Cannot set API key for hosted model "${apiKeyInput.model}" - API keys are managed by the platform when using hosted models`,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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]
|
||||
|
||||
// Handle nested block credential removal
|
||||
if (credInput.nestedBlockId) {
|
||||
const nestedNodes = op.params?.nestedNodes as
|
||||
| Record<string, Record<string, unknown>>
|
||||
| undefined
|
||||
const nestedBlock = nestedNodes?.[credInput.nestedBlockId]
|
||||
const nestedInputs = nestedBlock?.inputs as Record<string, unknown> | undefined
|
||||
if (nestedInputs?.[credInput.fieldName]) {
|
||||
delete nestedInputs[credInput.fieldName]
|
||||
logger.info('Removed invalid credential from nested block', {
|
||||
parentBlockId: credInput.blockId,
|
||||
nestedBlockId: credInput.nestedBlockId,
|
||||
field: credInput.fieldName,
|
||||
invalidValue: credInput.value,
|
||||
})
|
||||
}
|
||||
} else if (op.params?.inputs?.[credInput.fieldName]) {
|
||||
// Handle main block credential removal
|
||||
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}` : ''
|
||||
const errorBlockId = credInput.nestedBlockId ?? credInput.blockId
|
||||
errors.push({
|
||||
blockId: errorBlockId,
|
||||
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<string, Record<string, any>> }> {
|
||||
@@ -2950,29 +2657,12 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
|
||||
// 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 },
|
||||
workflowState
|
||||
)
|
||||
operationsToApply = filteredOperations
|
||||
credentialErrors.push(...credErrors)
|
||||
}
|
||||
|
||||
// Apply operations directly to the workflow state
|
||||
const {
|
||||
state: modifiedWorkflowState,
|
||||
validationErrors,
|
||||
skippedItems,
|
||||
} = applyOperationsToWorkflowState(workflowState, operationsToApply, permissionConfig)
|
||||
|
||||
// Add credential validation errors
|
||||
validationErrors.push(...credentialErrors)
|
||||
} = applyOperationsToWorkflowState(workflowState, operations, permissionConfig)
|
||||
|
||||
// Get workspaceId for selector validation
|
||||
let workspaceId: string | undefined
|
||||
|
||||
Reference in New Issue
Block a user