mirror of
https://github.com/FlowiseAI/Flowise.git
synced 2026-10-12 02:02:17 +00:00
feat(agentflow): strip server-only metadata from node data (#5966)
* feat(agentflow): strip server-only metadata from node data * feat(agentflow): enhance export flow data handling by stripping sensitive and runtime-only fields * address gemini review comment
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
import { makeFlowEdge, makeFlowNode } from '@test-utils/factories'
|
||||
|
||||
import type { FlowEdge, FlowNode } from '../types'
|
||||
import { FlowEdge, FlowNode } from '../types'
|
||||
|
||||
import { generateExportFlowData } from './flowExport'
|
||||
|
||||
@@ -34,19 +34,103 @@ describe('generateExportFlowData', () => {
|
||||
expect(result.nodes[0].data.credential).toBeUndefined()
|
||||
})
|
||||
|
||||
it('should preserve other node data', () => {
|
||||
it('should preserve allowlisted node data fields', () => {
|
||||
const flowData = {
|
||||
nodes: [
|
||||
makeNode('a', {
|
||||
data: { id: 'a', name: 'llm', label: 'LLM', inputValues: { model: 'gpt-4' } }
|
||||
data: {
|
||||
id: 'a',
|
||||
name: 'llm',
|
||||
label: 'LLM',
|
||||
version: 1,
|
||||
type: 'Agent',
|
||||
color: '#ff0000',
|
||||
hideInput: false,
|
||||
baseClasses: ['BaseLLM'],
|
||||
category: 'Agent Flows',
|
||||
description: 'An LLM node',
|
||||
icon: 'llm.svg',
|
||||
inputs: [{ id: 'i1', name: 'model', label: 'Model', type: 'string' }],
|
||||
inputValues: { model: 'gpt-4' },
|
||||
inputAnchors: [],
|
||||
outputAnchors: [],
|
||||
outputs: []
|
||||
}
|
||||
})
|
||||
],
|
||||
edges: []
|
||||
}
|
||||
const result = generateExportFlowData(flowData)
|
||||
expect(result.nodes[0].data.name).toBe('llm')
|
||||
expect(result.nodes[0].data.inputValues).toEqual({ model: 'gpt-4' })
|
||||
expect(result.nodes[0].position).toEqual({ x: 0, y: 0 })
|
||||
const data = result.nodes[0].data
|
||||
expect(data.name).toBe('llm')
|
||||
expect(data.label).toBe('LLM')
|
||||
expect(data.version).toBe(1)
|
||||
expect(data.type).toBe('Agent')
|
||||
expect(data.color).toBe('#ff0000')
|
||||
expect(data.icon).toBe('llm.svg')
|
||||
expect(data.category).toBe('Agent Flows')
|
||||
expect(data.description).toBe('An LLM node')
|
||||
expect(data.inputValues).toEqual({ model: 'gpt-4' })
|
||||
})
|
||||
|
||||
it('should strip runtime-only state from exported data', () => {
|
||||
const flowData = {
|
||||
nodes: [
|
||||
makeNode('a', {
|
||||
data: {
|
||||
id: 'a',
|
||||
name: 'llm',
|
||||
label: 'LLM',
|
||||
status: 'FINISHED',
|
||||
error: 'some error',
|
||||
warning: 'some warning',
|
||||
hint: 'a hint',
|
||||
validationErrors: ['err1']
|
||||
} as FlowNode['data']
|
||||
})
|
||||
],
|
||||
edges: []
|
||||
}
|
||||
const result = generateExportFlowData(flowData)
|
||||
const data = result.nodes[0].data
|
||||
expect(data).not.toHaveProperty('status')
|
||||
expect(data).not.toHaveProperty('error')
|
||||
expect(data).not.toHaveProperty('warning')
|
||||
expect(data).not.toHaveProperty('hint')
|
||||
expect(data).not.toHaveProperty('validationErrors')
|
||||
})
|
||||
|
||||
it('should strip password, file, and folder input values', () => {
|
||||
const flowData = {
|
||||
nodes: [
|
||||
makeNode('a', {
|
||||
data: {
|
||||
id: 'a',
|
||||
name: 'llm',
|
||||
label: 'LLM',
|
||||
inputs: [
|
||||
{ id: 'i1', name: 'apiKey', label: 'API Key', type: 'password' },
|
||||
{ id: 'i2', name: 'upload', label: 'Upload', type: 'file' },
|
||||
{ id: 'i3', name: 'dir', label: 'Directory', type: 'folder' },
|
||||
{ id: 'i4', name: 'model', label: 'Model', type: 'string' }
|
||||
],
|
||||
inputValues: {
|
||||
apiKey: 'sk-secret',
|
||||
upload: 'base64data',
|
||||
dir: '/some/path',
|
||||
model: 'gpt-4'
|
||||
}
|
||||
} as FlowNode['data']
|
||||
})
|
||||
],
|
||||
edges: []
|
||||
}
|
||||
const result = generateExportFlowData(flowData)
|
||||
const values = result.nodes[0].data.inputValues!
|
||||
expect(values).not.toHaveProperty('apiKey')
|
||||
expect(values).not.toHaveProperty('upload')
|
||||
expect(values).not.toHaveProperty('dir')
|
||||
expect(values.model).toBe('gpt-4')
|
||||
})
|
||||
|
||||
it('should not mutate the original flow data', () => {
|
||||
|
||||
@@ -1,18 +1,60 @@
|
||||
import type { FlowEdge, FlowNode } from '../types'
|
||||
import type { FlowEdge, FlowNode, NodeData } from '../types'
|
||||
|
||||
/** Sensitive input types that must not appear in exported flow data. */
|
||||
const SENSITIVE_INPUT_TYPES = new Set(['password', 'file', 'folder'])
|
||||
|
||||
// TODO: Integrate with save/export flow to strip credentials before persisting
|
||||
/**
|
||||
* Generate export-friendly flow data (strips sensitive info)
|
||||
* Build an allowlisted copy of node data for export.
|
||||
* Mirrors the field allowlist in agentflow v2's generateExportFlowData
|
||||
* (packages/ui/src/utils/genericHelper.js) and strips:
|
||||
* - credential references
|
||||
* - password / file / folder input values
|
||||
* - runtime-only state (status, error, warning, hint, validationErrors)
|
||||
*/
|
||||
function pickExportNodeData(data: NodeData): NodeData {
|
||||
const exported: NodeData = {
|
||||
id: data.id,
|
||||
name: data.name,
|
||||
label: data.label,
|
||||
version: data.version,
|
||||
type: data.type,
|
||||
color: data.color,
|
||||
hideInput: data.hideInput,
|
||||
baseClasses: data.baseClasses,
|
||||
category: data.category,
|
||||
description: data.description,
|
||||
inputs: data.inputs,
|
||||
inputAnchors: data.inputAnchors,
|
||||
outputAnchors: data.outputAnchors,
|
||||
outputs: data.outputs,
|
||||
icon: data.icon
|
||||
}
|
||||
|
||||
// Strip sensitive values from inputValues (password, file, folder)
|
||||
if (data.inputValues) {
|
||||
const inputDefsByName = new Map((data.inputs || []).map((i) => [i.name, i]))
|
||||
const cleanedValues: Record<string, unknown> = {}
|
||||
for (const [key, value] of Object.entries(data.inputValues)) {
|
||||
const inputDef = inputDefsByName.get(key)
|
||||
if (inputDef && SENSITIVE_INPUT_TYPES.has(inputDef.type)) continue
|
||||
cleanedValues[key] = value
|
||||
}
|
||||
exported.inputValues = cleanedValues
|
||||
}
|
||||
|
||||
return exported
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate export-friendly flow data.
|
||||
* Uses an explicit allowlist (matching agentflow v2 behaviour) so that
|
||||
* server-only metadata and sensitive values never leak into exports.
|
||||
*/
|
||||
export function generateExportFlowData(flowData: { nodes: FlowNode[]; edges: FlowEdge[] }): { nodes: FlowNode[]; edges: FlowEdge[] } {
|
||||
const nodes = flowData.nodes.map((node) => ({
|
||||
...node,
|
||||
selected: false,
|
||||
data: {
|
||||
...node.data,
|
||||
// Remove credential IDs for export
|
||||
credential: undefined
|
||||
}
|
||||
data: pickExportNodeData(node.data)
|
||||
}))
|
||||
|
||||
const edges = flowData.edges.map((edge) => ({
|
||||
|
||||
@@ -163,6 +163,40 @@ describe('initNode', () => {
|
||||
expect(result.outputAnchors).toHaveLength(0)
|
||||
})
|
||||
|
||||
it('should strip server-only metadata like filePath from node data', () => {
|
||||
const nodeData = makeNodeData({
|
||||
filePath: '/some/server/path/Agent.js',
|
||||
badge: 'NEW',
|
||||
author: 'Flowise',
|
||||
documentation: 'https://docs.example.com',
|
||||
loadMethods: { listModels: () => Promise.resolve([]) }
|
||||
} as Partial<NodeData>)
|
||||
const result = initNode(nodeData, 'n1')
|
||||
expect(result).not.toHaveProperty('filePath')
|
||||
expect(result).not.toHaveProperty('badge')
|
||||
expect(result).not.toHaveProperty('author')
|
||||
expect(result).not.toHaveProperty('documentation')
|
||||
expect(result).not.toHaveProperty('loadMethods')
|
||||
})
|
||||
|
||||
it('should strip runtime-only state from node data', () => {
|
||||
const nodeData = makeNodeData({
|
||||
status: 'FINISHED',
|
||||
error: 'some error',
|
||||
warning: 'some warning',
|
||||
hint: 'some hint',
|
||||
validationErrors: ['error1'],
|
||||
selected: true
|
||||
} as Partial<NodeData>)
|
||||
const result = initNode(nodeData, 'n1')
|
||||
expect(result).not.toHaveProperty('status')
|
||||
expect(result).not.toHaveProperty('error')
|
||||
expect(result).not.toHaveProperty('warning')
|
||||
expect(result).not.toHaveProperty('hint')
|
||||
expect(result).not.toHaveProperty('validationErrors')
|
||||
expect(result).not.toHaveProperty('selected')
|
||||
})
|
||||
|
||||
it('should generate dynamic outputAnchors for conditionAgentflow nodes', () => {
|
||||
const conditionNodeData = makeNodeData({
|
||||
name: 'conditionAgentflow',
|
||||
|
||||
@@ -88,6 +88,36 @@ function createAgentFlowOutputs(nodeData: NodeData, newNodeId: string): Array<{
|
||||
]
|
||||
}
|
||||
|
||||
/**
|
||||
* Pick only the properties that belong to NodeData from a server API response.
|
||||
* Strips server-only metadata (filePath, badge, author, loadMethods, etc.)
|
||||
* and runtime-only state (status, error, warning, hint, validationErrors)
|
||||
* that should not be persisted in flow data.
|
||||
*
|
||||
* Mirrors the allowlist used by generateExportFlowData in agentflow v2
|
||||
* (packages/ui/src/utils/genericHelper.js).
|
||||
*/
|
||||
function pickNodeData(raw: NodeData): NodeData {
|
||||
return {
|
||||
id: raw.id,
|
||||
name: raw.name,
|
||||
label: raw.label,
|
||||
type: raw.type,
|
||||
category: raw.category,
|
||||
description: raw.description,
|
||||
version: raw.version,
|
||||
baseClasses: raw.baseClasses,
|
||||
inputs: raw.inputs,
|
||||
inputValues: raw.inputValues,
|
||||
outputs: raw.outputs,
|
||||
inputAnchors: raw.inputAnchors,
|
||||
outputAnchors: raw.outputAnchors,
|
||||
color: raw.color,
|
||||
icon: raw.icon,
|
||||
hideInput: raw.hideInput
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a node with proper anchors and default values
|
||||
* Converts API response (with inputs as definitions) to canvas node format
|
||||
@@ -148,9 +178,9 @@ export function initNode(nodeData: NodeData, newNodeId: string, isAgentflow = tr
|
||||
}
|
||||
}
|
||||
|
||||
// Create initialized node data
|
||||
// Create initialized node data — pickNodeData strips server-only metadata
|
||||
const initializedData: NodeData = {
|
||||
...nodeData,
|
||||
...pickNodeData(nodeData),
|
||||
id: newNodeId,
|
||||
inputs: inputDefinitions, // Keep parameter definitions
|
||||
inputValues: { ...initialInputValues, ...(nodeData.inputValues || {}) }, // Merge defaults with existing values
|
||||
|
||||
Reference in New Issue
Block a user