mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-26 07:18:38 -05:00
Compare commits
39 Commits
fix/copilo
...
fix/copilo
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f0ae238be | ||
|
|
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 |
@@ -2508,6 +2508,10 @@ async function validateWorkflowSelectorIds(
|
|||||||
for (const subBlockConfig of blockConfig.subBlocks) {
|
for (const subBlockConfig of blockConfig.subBlocks) {
|
||||||
if (!SELECTOR_TYPES.has(subBlockConfig.type)) continue
|
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
|
const subBlockValue = blockData.subBlocks?.[subBlockConfig.id]?.value
|
||||||
if (!subBlockValue) continue
|
if (!subBlockValue) continue
|
||||||
|
|
||||||
@@ -2573,6 +2577,157 @@ async function validateWorkflowSelectorIds(
|
|||||||
return errors
|
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) {
|
||||||
|
op.params.inputs.apiKey = undefined
|
||||||
|
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(
|
async function getCurrentWorkflowStateFromDb(
|
||||||
workflowId: string
|
workflowId: string
|
||||||
): Promise<{ workflowState: any; subBlockValues: Record<string, Record<string, any>> }> {
|
): Promise<{ workflowState: any; subBlockValues: Record<string, Record<string, any>> }> {
|
||||||
@@ -2657,12 +2812,28 @@ export const editWorkflowServerTool: BaseServerTool<EditWorkflowParams, any> = {
|
|||||||
// Get permission config for the user
|
// Get permission config for the user
|
||||||
const permissionConfig = context?.userId ? await getUserPermissionConfig(context.userId) : null
|
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
|
// Apply operations directly to the workflow state
|
||||||
const {
|
const {
|
||||||
state: modifiedWorkflowState,
|
state: modifiedWorkflowState,
|
||||||
validationErrors,
|
validationErrors,
|
||||||
skippedItems,
|
skippedItems,
|
||||||
} = applyOperationsToWorkflowState(workflowState, operations, permissionConfig)
|
} = applyOperationsToWorkflowState(workflowState, operationsToApply, permissionConfig)
|
||||||
|
|
||||||
|
// Add credential validation errors
|
||||||
|
validationErrors.push(...credentialErrors)
|
||||||
|
|
||||||
// Get workspaceId for selector validation
|
// Get workspaceId for selector validation
|
||||||
let workspaceId: string | undefined
|
let workspaceId: string | undefined
|
||||||
|
|||||||
Reference in New Issue
Block a user