Skip to content

Commit 9e92d60

Browse files
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
1 parent 65bfd31 commit 9e92d60

4 files changed

Lines changed: 206 additions & 16 deletions

File tree

packages/agentflow/src/core/utils/flowExport.test.ts

Lines changed: 90 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { makeFlowEdge, makeFlowNode } from '@test-utils/factories'
22

3-
import type { FlowEdge, FlowNode } from '../types'
3+
import { FlowEdge, FlowNode } from '../types'
44

55
import { generateExportFlowData } from './flowExport'
66

@@ -34,19 +34,103 @@ describe('generateExportFlowData', () => {
3434
expect(result.nodes[0].data.credential).toBeUndefined()
3535
})
3636

37-
it('should preserve other node data', () => {
37+
it('should preserve allowlisted node data fields', () => {
3838
const flowData = {
3939
nodes: [
4040
makeNode('a', {
41-
data: { id: 'a', name: 'llm', label: 'LLM', inputValues: { model: 'gpt-4' } }
41+
data: {
42+
id: 'a',
43+
name: 'llm',
44+
label: 'LLM',
45+
version: 1,
46+
type: 'Agent',
47+
color: '#ff0000',
48+
hideInput: false,
49+
baseClasses: ['BaseLLM'],
50+
category: 'Agent Flows',
51+
description: 'An LLM node',
52+
icon: 'llm.svg',
53+
inputs: [{ id: 'i1', name: 'model', label: 'Model', type: 'string' }],
54+
inputValues: { model: 'gpt-4' },
55+
inputAnchors: [],
56+
outputAnchors: [],
57+
outputs: []
58+
}
4259
})
4360
],
4461
edges: []
4562
}
4663
const result = generateExportFlowData(flowData)
47-
expect(result.nodes[0].data.name).toBe('llm')
48-
expect(result.nodes[0].data.inputValues).toEqual({ model: 'gpt-4' })
49-
expect(result.nodes[0].position).toEqual({ x: 0, y: 0 })
64+
const data = result.nodes[0].data
65+
expect(data.name).toBe('llm')
66+
expect(data.label).toBe('LLM')
67+
expect(data.version).toBe(1)
68+
expect(data.type).toBe('Agent')
69+
expect(data.color).toBe('#ff0000')
70+
expect(data.icon).toBe('llm.svg')
71+
expect(data.category).toBe('Agent Flows')
72+
expect(data.description).toBe('An LLM node')
73+
expect(data.inputValues).toEqual({ model: 'gpt-4' })
74+
})
75+
76+
it('should strip runtime-only state from exported data', () => {
77+
const flowData = {
78+
nodes: [
79+
makeNode('a', {
80+
data: {
81+
id: 'a',
82+
name: 'llm',
83+
label: 'LLM',
84+
status: 'FINISHED',
85+
error: 'some error',
86+
warning: 'some warning',
87+
hint: 'a hint',
88+
validationErrors: ['err1']
89+
} as FlowNode['data']
90+
})
91+
],
92+
edges: []
93+
}
94+
const result = generateExportFlowData(flowData)
95+
const data = result.nodes[0].data
96+
expect(data).not.toHaveProperty('status')
97+
expect(data).not.toHaveProperty('error')
98+
expect(data).not.toHaveProperty('warning')
99+
expect(data).not.toHaveProperty('hint')
100+
expect(data).not.toHaveProperty('validationErrors')
101+
})
102+
103+
it('should strip password, file, and folder input values', () => {
104+
const flowData = {
105+
nodes: [
106+
makeNode('a', {
107+
data: {
108+
id: 'a',
109+
name: 'llm',
110+
label: 'LLM',
111+
inputs: [
112+
{ id: 'i1', name: 'apiKey', label: 'API Key', type: 'password' },
113+
{ id: 'i2', name: 'upload', label: 'Upload', type: 'file' },
114+
{ id: 'i3', name: 'dir', label: 'Directory', type: 'folder' },
115+
{ id: 'i4', name: 'model', label: 'Model', type: 'string' }
116+
],
117+
inputValues: {
118+
apiKey: 'sk-secret',
119+
upload: 'base64data',
120+
dir: '/some/path',
121+
model: 'gpt-4'
122+
}
123+
} as FlowNode['data']
124+
})
125+
],
126+
edges: []
127+
}
128+
const result = generateExportFlowData(flowData)
129+
const values = result.nodes[0].data.inputValues!
130+
expect(values).not.toHaveProperty('apiKey')
131+
expect(values).not.toHaveProperty('upload')
132+
expect(values).not.toHaveProperty('dir')
133+
expect(values.model).toBe('gpt-4')
50134
})
51135

52136
it('should not mutate the original flow data', () => {

packages/agentflow/src/core/utils/flowExport.ts

Lines changed: 50 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,60 @@
1-
import type { FlowEdge, FlowNode } from '../types'
1+
import type { FlowEdge, FlowNode, NodeData } from '../types'
2+
3+
/** Sensitive input types that must not appear in exported flow data. */
4+
const SENSITIVE_INPUT_TYPES = new Set(['password', 'file', 'folder'])
25

3-
// TODO: Integrate with save/export flow to strip credentials before persisting
46
/**
5-
* Generate export-friendly flow data (strips sensitive info)
7+
* Build an allowlisted copy of node data for export.
8+
* Mirrors the field allowlist in agentflow v2's generateExportFlowData
9+
* (packages/ui/src/utils/genericHelper.js) and strips:
10+
* - credential references
11+
* - password / file / folder input values
12+
* - runtime-only state (status, error, warning, hint, validationErrors)
13+
*/
14+
function pickExportNodeData(data: NodeData): NodeData {
15+
const exported: NodeData = {
16+
id: data.id,
17+
name: data.name,
18+
label: data.label,
19+
version: data.version,
20+
type: data.type,
21+
color: data.color,
22+
hideInput: data.hideInput,
23+
baseClasses: data.baseClasses,
24+
category: data.category,
25+
description: data.description,
26+
inputs: data.inputs,
27+
inputAnchors: data.inputAnchors,
28+
outputAnchors: data.outputAnchors,
29+
outputs: data.outputs,
30+
icon: data.icon
31+
}
32+
33+
// Strip sensitive values from inputValues (password, file, folder)
34+
if (data.inputValues) {
35+
const inputDefsByName = new Map((data.inputs || []).map((i) => [i.name, i]))
36+
const cleanedValues: Record<string, unknown> = {}
37+
for (const [key, value] of Object.entries(data.inputValues)) {
38+
const inputDef = inputDefsByName.get(key)
39+
if (inputDef && SENSITIVE_INPUT_TYPES.has(inputDef.type)) continue
40+
cleanedValues[key] = value
41+
}
42+
exported.inputValues = cleanedValues
43+
}
44+
45+
return exported
46+
}
47+
48+
/**
49+
* Generate export-friendly flow data.
50+
* Uses an explicit allowlist (matching agentflow v2 behaviour) so that
51+
* server-only metadata and sensitive values never leak into exports.
652
*/
753
export function generateExportFlowData(flowData: { nodes: FlowNode[]; edges: FlowEdge[] }): { nodes: FlowNode[]; edges: FlowEdge[] } {
854
const nodes = flowData.nodes.map((node) => ({
955
...node,
1056
selected: false,
11-
data: {
12-
...node.data,
13-
// Remove credential IDs for export
14-
credential: undefined
15-
}
57+
data: pickExportNodeData(node.data)
1658
}))
1759

1860
const edges = flowData.edges.map((edge) => ({

packages/agentflow/src/core/utils/nodeFactory.test.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,40 @@ describe('initNode', () => {
163163
expect(result.outputAnchors).toHaveLength(0)
164164
})
165165

166+
it('should strip server-only metadata like filePath from node data', () => {
167+
const nodeData = makeNodeData({
168+
filePath: '/some/server/path/Agent.js',
169+
badge: 'NEW',
170+
author: 'Flowise',
171+
documentation: 'https://docs.example.com',
172+
loadMethods: { listModels: () => Promise.resolve([]) }
173+
} as Partial<NodeData>)
174+
const result = initNode(nodeData, 'n1')
175+
expect(result).not.toHaveProperty('filePath')
176+
expect(result).not.toHaveProperty('badge')
177+
expect(result).not.toHaveProperty('author')
178+
expect(result).not.toHaveProperty('documentation')
179+
expect(result).not.toHaveProperty('loadMethods')
180+
})
181+
182+
it('should strip runtime-only state from node data', () => {
183+
const nodeData = makeNodeData({
184+
status: 'FINISHED',
185+
error: 'some error',
186+
warning: 'some warning',
187+
hint: 'some hint',
188+
validationErrors: ['error1'],
189+
selected: true
190+
} as Partial<NodeData>)
191+
const result = initNode(nodeData, 'n1')
192+
expect(result).not.toHaveProperty('status')
193+
expect(result).not.toHaveProperty('error')
194+
expect(result).not.toHaveProperty('warning')
195+
expect(result).not.toHaveProperty('hint')
196+
expect(result).not.toHaveProperty('validationErrors')
197+
expect(result).not.toHaveProperty('selected')
198+
})
199+
166200
it('should generate dynamic outputAnchors for conditionAgentflow nodes', () => {
167201
const conditionNodeData = makeNodeData({
168202
name: 'conditionAgentflow',

packages/agentflow/src/core/utils/nodeFactory.ts

Lines changed: 32 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,36 @@ function createAgentFlowOutputs(nodeData: NodeData, newNodeId: string): Array<{
8888
]
8989
}
9090

91+
/**
92+
* Pick only the properties that belong to NodeData from a server API response.
93+
* Strips server-only metadata (filePath, badge, author, loadMethods, etc.)
94+
* and runtime-only state (status, error, warning, hint, validationErrors)
95+
* that should not be persisted in flow data.
96+
*
97+
* Mirrors the allowlist used by generateExportFlowData in agentflow v2
98+
* (packages/ui/src/utils/genericHelper.js).
99+
*/
100+
function pickNodeData(raw: NodeData): NodeData {
101+
return {
102+
id: raw.id,
103+
name: raw.name,
104+
label: raw.label,
105+
type: raw.type,
106+
category: raw.category,
107+
description: raw.description,
108+
version: raw.version,
109+
baseClasses: raw.baseClasses,
110+
inputs: raw.inputs,
111+
inputValues: raw.inputValues,
112+
outputs: raw.outputs,
113+
inputAnchors: raw.inputAnchors,
114+
outputAnchors: raw.outputAnchors,
115+
color: raw.color,
116+
icon: raw.icon,
117+
hideInput: raw.hideInput
118+
}
119+
}
120+
91121
/**
92122
* Initialize a node with proper anchors and default values
93123
* Converts API response (with inputs as definitions) to canvas node format
@@ -148,9 +178,9 @@ export function initNode(nodeData: NodeData, newNodeId: string, isAgentflow = tr
148178
}
149179
}
150180

151-
// Create initialized node data
181+
// Create initialized node data — pickNodeData strips server-only metadata
152182
const initializedData: NodeData = {
153-
...nodeData,
183+
...pickNodeData(nodeData),
154184
id: newNodeId,
155185
inputs: inputDefinitions, // Keep parameter definitions
156186
inputValues: { ...initialInputValues, ...(nodeData.inputValues || {}) }, // Merge defaults with existing values

0 commit comments

Comments
 (0)